اذهب إلى المحتوى

السمات Traits في لغة رست Rust


Naser Dakhel

يمكن أن تعرّف السمة وظيفة نوع محدد ويمكن مشاركتها مع عدّة أنواع، ويمكننا استخدام السمات لتعريف سلوك مشترك بطريقة مجردة، ويمكننا استخدام حدود السمة 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...