يمكن أن تعرّف السمة وظيفة نوع محدد ويمكن مشاركتها مع عدّة أنواع، ويمكننا استخدام السمات لتعريف سلوك مشترك بطريقة مجردة، ويمكننا استخدام حدود السمة trait bounds لتحديد أن النوع المعمّم يمكن أن يكون أي نوع يمتلك سلوكًا محددًا.
ملاحظة: السمات مشابهة لميزة تدعى الواجهات interfaces في لغات برمجة أخرى، إلا أن هناك بعض الاختلافات.
تعريف سمة Trait
يتكون سلوك النوع من توابع يمكننا استدعائها على هذا النوع، ونقول أن عدّة أنواع تشارك السلوك ذاته إذا أمكننا استدعاء التوابع ذاتها على جميع هذه الأنواع، ويُعد تعريف السمة طريقةً لجمع بصمات التوابع method signatures لتعريف مجموعة من السلوكيات المهمة لتحقيق غرض ما.
على سبيل المثال، دعنا نفترض وجود عدّة هياكل تحمل أنواع وكميات مختلفة من النص، إذ يحمل الهيكل NewsArticle
حقلًا لمحتوى إخباري في موقع معين، ويمكن أن تحتوي Tweet
على نص طوله 280 محرفًا بالحد الأعظمي، إضافةً إلى البيانات الوصفية metadata التي تشير إلى كون التغريدة جديدة، أو إعادة تغريد retweet، أو رد على تغريدة أخرى.
نريد أن نُنشئ وحدة مكتبة مصرَّفة library crate تجمع الأخبار تدعى aggregator
، بحيث تعرض ملخصًا للبيانات التي قد تجدها في نسخ NewsArticle
أو Tweet
، ثمّ سنستدعي الملخص لأي من النسخ باستدعاء التابع summarize
. توضح الشيفرة 12 تعريف السمة العامة Summary
التي تعبّر عن هذا السلوك.
اسم الملف: src/lib.rs
pub trait Summary { fn summarize(&self) -> String; }
الشيفرة 12: سمة Summary
تتألف من السلوك الموجود في التابع summarize
نصرّح هنا عن سمة باستخدام الكلمة المفتاحية trait
متبوعةً باسم السمة وهي Summary
في هذه الحالة، كما نصرّح أيضًا عن السمة بكونها عامة pub
بحيث تستخدم الوحدات المصرّفة هذه الوحدة المصرفة كما سنرى في الأمثلة القادمة. نصرّح عن بصمات التابع داخل القوسين المعقوصين curly brackets، إذ تصف البصمات سلوك الأنواع التي تطبق هذه السمة، والتي هي في هذه الحالة fn summarize(&self) -> String
.
بعد التصريح عن بصمة التابع، يمكننا استخدام الفاصلة المنقوطة بدلًا من الأقواس المعقوصة. ويجب على كل نوع ينفّذ هذه السمة أن يوفّر سلوكه المخصص لمتن التابع. سيفرض المصرّف أن أي نوع له السمة Summary
سيكون له تابع باسم summarize
مُعرّف بتلك البصمة المُحدَّدة.
يمكن أن تحتوي السمة عدّة توابع في متنها، إذ أن بصمات التوابع محتواة في كل سطر على حدة، وينتهي كل سطر بفاصلة منقوطة.
تطبيق السمة على نوع
الآن وبعد أن عرَّفنا البصمات المطلوبة لتوابع السمة Summary
يمكننا تطبيقها على الأنواع الموجودة في مجمّع الوسائط media aggregator. توضح الشيفرة 13 تنفيذًا للسمة Summary
في الهيكل NewsArticle
الذي يستخدم كل من العنوان والمؤلف والمكان لإنشاء قيمة مُعادة من summarize
. نعرّف من أجل الهيكل Tweet
الدالة summarize
بحيث تحصل على اسم المستخدم متبوعًا بالنص الكامل الموجود في التغريدة وذلك بفرض أن التغريدة محدودة بمقدار 280 محرف.
اسم الملف: src/lib.rs
pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } }
الشيفرة 13: تطبيق السمة Summary
على كل من النوعين NewsArticle
و Tweet
تطبيق سمة على نوع هي عملية مشابهة لتطبيق توابع اعتيادية، إلا أن الفارق هنا هو أننا نضع اسم السمة التي نريد تطبيقها بعد impl
، ثم نستخدم الكلمة المفتاحية for
ونحدد اسم النوع الذي نريد تطبيق السمة عليه. نضع داخل كتلة impl
بصمات التابع المعرفة في تعريف السمة، وبدلًا من إضافة الفاصلة المنقوطة بعد كل بصمة سنستخدم الأقواس المعقوصة ونملأ داخلها متن التابع مع السلوك المخصص الذي نريد من توابع السمة أن تمتلكه لنوع معين.
الآن وبعد أن طبّقنا السمة Summary
في وحدة المكتبة المصرفة على NewsArtilce
و Tweet
، يمكن لمستخدمي الوحدة المصرّفة استدعاء توابع السمة على نسخٍ من NewsArticle
و Tweet
بالطريقة ذاتها التي نستدعي بها توابع اعتيادية، إلا أن الفارق الوحيد هنا هو أن المستخدم يجب أن يُضيف السمة إلى النطاق scope إضافةً إلى الأنواع. إليك مثالًا عن كيفية استخدام وحدة المكتبة المصرفة aggregator
من قِبل وحدة ثنائية مصرّفة binary crate:
use aggregator::{Summary, Tweet}; fn main() { let tweet = Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }; println!("1 new tweet: {}", tweet.summarize()); }
تطبع الشيفرة البرمجية السابقة ما يلي:
1 new tweet: horse_ebooks: of course, as you probably already know, people
يُمكن أن تضيف الوحدات المصرّفة الأخرى المعتمدة على الوحدة المصرفة aggregator
السمة Summary
إلى النطاق لتطبيق Summary
على أنواعها الخاصة، إلا أن القيد الوحيد هنا الذي يجب ملاحظته هو أنه يمكننا تطبيق السمة على نوع نريده فقط إذا كانت سمة واحدة على الأقل أو نوعًا واحدًا على الأقل محليًا local بالنسبة لوحدتنا المصرّفة؛ إذ يمكننا على سبيل المثال تطبيق سمات المكتبة القياسية مثل Display
على نوع مخصص مثل Tweet
بمثابة جزء من وظيفة وحدتنا المصرفة aggregator
، لأن النوع Tweet
هو محلي بالنسبة إلى الوحدة المصرفة aggregator
، كما يمكننا أيضًا تطبيق Summary
على النوع Vec<T>
في الوحدة المصرفة aggregator
لأن السمة Summary
هي سمة محلية بالنسبة لوحدتنا المصرفة aggregator
.
في المقابل، لا يمكننا تطبيق سمة خارجية على أنواع خارجية، فعلى سبيل المثال لا يمكننا تطبيق السمة Display
على النوع Vec<T>
داخل الوحدة المصرفة aggregator
، لأن Display
و Vec<T>
ليستا معرفتين في المكتبة القياسية أو محليةً بالنسبة للوحدة المصرفة aggregator
. يُعد هذا القيد جزءًا من خاصية تدعى الترابط المنطقي coherence وبالأخص قاعدة اليتيم orphan rule وتسمى القاعدة بهذا الاسم لأن نوع الأب غير موجود، وتتأكد هذه القاعدة من أن الشيفرة البرمجية الخاصة بالمبرمجين الآخرين لن تتسبب بعطل شيفرتك البرمجية والعكس صحيح، وبدون هذه القاعدة يمكن للوحدتين المصرفتين تطبيق السمة ذاتها على النوع ذاته، وعندها لن تستطيع رست معرفة أي من التنفيذين يجب استخدامه.
التنفيذات الافتراضية
من المفيد في بعض الأحيان تواجد سلوك افتراضي لبعض التوابع الموجودة في سمة ما أو جميعها بدلًا من طلب كتابة متن لكل التوابع ضمن كل نوع، بحيث يمكننا إعادة الكتابة على السلوك الافتراضي للتابع إذا أردنا تطبيق السمة على نوع معيّن.
نحدد في الشيفرة 14 سلسلةً نصيةً افتراضية للتابع summarize
ضمن السمة Summary
بدلًا من تعريف بصمة التابع كما فعلنا في الشيفرة 12.
اسم الملف: src/lib.rs
pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } }
الشيفرة 14: تعريف سمة Summary
بتنفيذ افتراضي خاص بالتابع summarize
نحدد كتلة impl
فارغة بكتابة impl Summary for NewsArticle {}
لاستخدام التنفيذ الافتراضي لتلخيص نسخ NewsArticle
.
على الرغم من أننا لا نعرف بعد الآن التابع summarize
على NewsArticle
مباشرةً إلا أننا قدمنا متنًا افتراضيًا وحددنا أن NewsArticle
تستخدم السمة Summary
، ونتيجةً لذلك يمكننا استدعاء التابع summarize
على نسخة من NewsArticle
كما يلي:
let article = NewsArticle { headline: String::from("Penguins win the Stanley Cup Championship!"), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), }; println!("New article available! {}", article.summarize());
تطبع الشيفرة البرمجية السابقة ما يلي:
New article available! (Read more...)
لا يتطلب إنشاء تنفيذ افتراضي تعديل أي شيء بخصوص تنفيذ Summary
على Tweet
في الشيفرة 13، وذلك لأن طريقة الكتابة على التنفيذ الافتراضي مماثلة لصيغة تنفيذ تابع سمة لا يحتوي على تنفيذ افتراضي.
يمكن أن تستدعي التنفيذات الافتراضية توابع أخرى في السمة ذاتها حتى لو كانت التوابع الأخرى لا تحتوي على تنفيذ افتراضي، وبذلك يمكن أن تقدم السمة الكثير من المزايا المفيدة باستخدامها لتنفيذٍ محدد في جزء صغير منها، على سبيل المثال يمكننا أن نعرف السمة Summary
بحيث تحتوي على تابع summarize_author
يحتوي على تنفيذ داخله ومن ثم تابع summarize
يحتوي على تنفيذٍ افتراضي يستدعي التابع summarize_author
:
pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } }
لاستخدام هذا الإصدار من Summary
علينا أن نعرف summarize_author
عند تطبيق السمة على النوع:
impl Summary for Tweet { fn summarize_author(&self) -> String { format!("@{}", self.username) } }
يمكننا استدعاء summarize
على نسخة من هيكل Tweet
بعد تعريفنا التابع summarize_author
، وعندها سيستدعي التنفيذ الافتراضي للتابع summarize
تعريف التابع summarize_author
الذي أضفناه، ولأننا كتبنا summarize_author
فنحن منحنا للسمة Summary
سلوكًا للتابع summarize
دون كتابة المزيد من الأسطر البرمجية.
let tweet = Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }; println!("1 new tweet: {}", tweet.summarize());
تطبع الشيفرة البرمجية السابقة ما يلي:
1 new tweet: (Read more from @horse_ebooks...)
لاحظ أنه ليس من الممكن استدعاء التنفيذ الافتراضي من تنفيذٍ كتبنا فوقه override لنفس التابع.
السمات مثل معاملات
الآن، وبعد أن تعلمنا كيفية تعريف وتطبيق السمات، أصبح بإمكاننا النظر إلى كيفية استخدام السمات لتعريف الدوال التي تقبل العديد من الأنواع المختلفة، وسنستخدم هنا السمة Summary
التي طبقناها على النوعين NewsArtilce
و Tweet
في الشيفرة 13 لتعريف الدالة notify
التي تستدعي التابع summarize
على المعامل item
وهو نوع ينفّذ السمة Summary
. لتحقيق ذلك علينا أن نكتب صيغة impl Trait
بالشكل التالي:
pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); }
بدلًا من استخدام نوع ثابت للمعامل item
نحدد الكلمة المفتاحية impl
ومن ثم اسم السمة، إذ يقبل هذا المعامل أي نوع ينفّذ السمة التي حددناها. يمكننا استدعاء أي تابع في notify
على item
يحتوي على السمة Summary
مثل summarize
، إذ يمكننا استدعاء notify
وتمرير أي نسخة من NewsArticle
أو Tweet
. لن تصرَّف الشيفرة البرمجية التي تستدعي الدالة باستخدام نوع آخر مثل String
أو i32
وذلك لأن الأنواع هذه لا تنفّذ Summary
.
صيغة حدود السمة
تكون صيغة impl Triat
جيدة للاستخدامات البسيطة، إلا أنها طريقة مختصرة عن طريقة أطول تُعرف بحدود السمة trait bound، وتبدو على النحو التالي:
pub fn notify<T: Summary>(item: &T) { println!("Breaking news! {}", item.summarize()); }
تماثل هذه الكتابة الطويلة الكتابة في القسم السابق إلا أنها أطول، إذ أننا نضع حدود السمة في تصريح معاملات النوع المعمم بعد النقطتين وداخل أقواس مثلثة angle brackets.
تُعد صيغة impl Trait
مناسبة وتجعل من شيفرتنا البرمجية أبسط في العديد من الحالات البسيطة إلا أن كتابة حدود السمة بشكلها الكامل تسمح لنا بتحديد تفاصيل أدق في بعض الحالات، على سبيل المثال يمكننا كتابة معاملين ينفّذان السمة Summary
وكتابة هذا الأمر بصيغة impl Trait
، وسيبدو بهذا الشكل:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
يُعد استخدام صيغة impl Trait
ملائمًا إذا أردنا لهذه الدالة السماح للمعاملين item1
و item2
أن يكونا من نوعين مختلفين (طالما ينفّذ كلاهما Summary
). إذا أردنا إجبار المعاملين على استخدام النوع ذاته يجب أن نستخدم حدود السمة على النحو التالي:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
يقيّد النوع المعمّم T
المحدد على أنه نوع لكل من المعاملين item1
و item2
الدالة بأنه يجب عليها قبول القيمتين فقط إذا كان كل من item1
و item2
لهما النوع ذاته.
تحديد حدود سمة عديدة باستخدام صيغة +
يمكننا تحديد أكثر من حد سمة واحد، لنقل أننا نريد notify
أن تستخدم تنسيق طباعة معيّن بالإضافة إلى summarize
على item
، عندها نحدد في تعريف notify
أنه يجب على item
أن تنفّذ كلًا من Display
و Summary
بنفس الوقت، ويمكننا فعل ذلك باستخدام الصيغة +
:
pub fn notify(item: &(impl Summary + Display)) {
الصيغة +
صالحة أيضًا مع حدود السمات على الأنواع المعممة:
pub fn notify<T: Summary + Display>(item: &T) {
يمكن لمتن الدالة notify
أن يستدعي summarize
مع استخدام {}
لتنسيق item
وذلك مع وجود حدّين للسمة.
حدود سمة أوضح باستخدام بنى where
لاستخدام حدود سمة عديدة بعض السلبيات إذ أن كل نوع معمم يحتوي على حد سمة خاص به، لذا من الممكن للدوال التي تحتوي على عدة أنواع معممة مثل معاملات أن تحتوي الكثير من المعلومات بخصوص حدود السمة بين اسم الدالة ولائحة معاملاتها مما يجعل بصمة الدالة صعبة القراءة، ولذلك تحتوي رست على طريقة كتابة بديلة لتحديد حدود السمة داخل بنية where
بعد بصمة الدالة، وبالتالي يمكننا استخدام البنية where
على النحو التالي:
fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug, {
بدلًا من كتابة التالي:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
أصبحت الآن بصمة الدالة أكثر وضوحًا إذ تحتوي على اسم الدالة ولائحة معاملاتها والنوع الذي تُعيده على سطر واحد بصورةٍ مشابهة لدالة لا تحتوي على الكثير من حدود السمة.
إعادة الأنواع التي تنفذ السمات
يمكننا أيضًا استخدام صيغة impl Triat
في مكان الإعادة لإعادة قيمة من نوع ما يطبّق سمة، كما هو موضح هنا:
fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } }
نستطيع تحديد أن الدالة returns_summarizable
تُعيد نوعًا يطبق السمة Summary
باستخدام impl Summary
على أنه نوع مُعاد دون تسمية النوع الثابت، وفي هذه الحالة تعيد الدالة returns_summarizable
القيمة Tweet
إلا أنه ليس من الضروري أن تعلم الشيفرة التي تستدعي الدالة بذلك.
إمكانية تحديد قيمة مُعادة فقط عن طريق السمة التي تطبقها مفيد جدًا، بالأخص في سياق المغلفات closures والمكررات iterators وهما مفهومان سنتكلم عنهما لاحقًا، إذ تُنشئ المغلفات والمكررات أنواعًا يعرفها المصرف فقط، أو أنواعًا يتطلب تحديدها كتابةً طويلةً إلا أن الصيغة impl Trait
تسمح لك بتحديد أن الدالة تُعيد نوعًا ما يطبّق السمة Iterator
دون الحاجة لكتابة نوع طويل.
يمكنك استخدام impl Trait
فقط في حال إعادتك لنوع واحد، على سبيل المثال تُعيد الشيفرة البرمجية التالية إما NewsArticle
، أو Tweet
بتحديد النوع المُعاد باستخدام impl Summary
إلا أن ذلك لا ينجح:
fn returns_summarizable(switch: bool) -> impl Summary { if switch { NewsArticle { headline: String::from( "Penguins win the Stanley Cup Championship!", ), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), } } else { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } } }
إعادة إما NewsArticle
أو Tweet
ليس مسموحًا بسبب القيود التي يفرضها استخدام الصيغة impl Trait
وكيفية تنفيذها في المصرّف، وسنتكلم لاحقًا عن كيفية كتابة دالة تحقق هذا السلوك لاحقًا.
استخدام حدود السمة لتنفيذ التوابع شرطيا
يمكننا تنفيذ التوابع شرطيًا للأنواع التي تنفّذ سمةً ما عند استخدام هذه السمة بواسطة كتلة impl
التي تستخدم الأنواع المعممة مثل معاملات. على سبيل المثال، ينفّذ النوع Pair<T>
في الشيفرة 15 الدالة new
دومًا لإعادة نسخةٍ جديدة من Pair<T>
(تذكر أن self
هو اسم نوع مستعار للنوع الموجود في الكتلة impl
وهو Pair<T>
في هذه الحالة)، إلا أنه في كتلة impl
التالية ينفّذ Pair<T>
التابع cmp_display
فقط إذا كان النوع T
الداخلي ينفّذ السمة PartialOrd
التي تمكّن المقارنة بالإضافة إلى سمة `Display التي تمكّن الطباعة.
اسم الملف: src/lib.rs
use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } }
الشيفرة 15: تنفيذ توابع شرطيًا على نوع معمّم بحسب حدود السمة
يمكننا أيضًا تنفيذ سمة شرطيًا لأي نوع ينفّذ سمةً أخرى، وتنفيذ السمة على أي نوع يحقق حدود السمة يسمّى بالتنفيذات الشاملة blanket implementations ويُستخدم بكثرة في مكتبة رست القياسية؛ على سبيل المثال تنفِّذ المكتبة القياسية السمة ToString
على أي نوع ينفّذ السمة Display
، وتبدو كتلة impl
في المكتبة القياسية بصورةٍ مشابهة لما يلي:
impl<T: Display> ToString for T { // --snip-- }
ولأن المكتبة القياسية تستخدم التنفيذ الشامل هذا فيمكننا استدعاء التابع to_string
المعرف باستخدام السمة ToString
على أي نوع ينفّذ السمة Display
على سبيل المثال يمكننا تحويل الأعداد الصحيحة إلى قيمة موافقة لها في النوع String
وذلك لأن الأعداد الصحيحة تنفّذ السمة Display
:
let s = 3.to_string();
يمكنك ملاحظة التنفيذات الشاملة في توثيق السمة في قسم "المنفّذين implementors".
تسمح لنا السمات وحدود السمات بكتابة شيفرة برمجية تستخدم الأنواع المعممة مثل معاملات، وذلك للتقليل من تكرار الشيفرة البرمجية، إضافةً إلى تحديدنا للمصرف بأننا نريد لقيمة معممة أن يكون لها سلوك معين، ويمكن للمصرف عندئذ استخدام معلومات حدود السمة للتحقق من أن جميع الأنواع الثابتة المستخدمة في شيفرتنا البرمجية تحتوي على السلوك الصحيح. سنحصل في لغات البرمجة المكتوبة ديناميكيًا dynamically typed على خطأ عند وقت التشغيل runtime إذا استدعينا تابعًا على نوع لم يعرّف هذا التابع، إلا أن رست تنقل هذه الأخطاء إلى وقت التصريف بحيث تجبرنا على تصحيح المشاكل قبل أن تُنفَّذ شيفرتنا البرمجية.
إضافةً لما سبق، لا يتوجب علينا كتابة شيفرة برمجية تتحقق من السلوك عند وقت التشغيل لأننا تحققنا من السلوك عند وقت التصريف، ويحسّن ذلك أداء الشيفرة البرمجية دون الحاجة للتخلي عن مرونة استخدام الأنواع المعممة.
ترجمة -وبتصرف- لقسم من الفصل Generic Types, Traits, and Lifetimes من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.