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

مفاهيم متقدمة عن السمات Trait في لغة رست


Naser Dakhel

غطينا مفهوم السمات سابقًا في فصل السمات Traits في لغة رست Rust، إلا أننا لم نناقش التفاصيل الأكثر تقدمًا. سنخوض الآن بالتفاصيل الجوهرية بعد أن تعملت المزيد عن لغة رست حتى الآن.

تحديد أنواع الموضع المؤقت في تعريفات السمات مع الأنواع المرتبطة

تصل الأنواع المرتبطة associated types نوع موضع مؤقت placeholder بسمة بحيث يمكن لتعريفات تابع السمة استخدام أنواع المواضع المؤقتة هذه في بصماتها signature، وسيحدد منفّذ السمة النوع الحقيقي الذي سيُستخدم بدلًا من نوع الموضع المؤقت للتنفيذ المعين. يمكننا بهذه الطريقة تحديد سمة تستخدم بعض الأنواع دون الحاجة إلى معرفة ماهية هذه الأنواع تحديدًا حتى تُطبَّق السمة.

وصفنا معظم الميزات المتقدمة في هذا الفصل على أنها نادرًا ما تكون مطلوبة. توجد الأنواع المرتبطة في مكان ما في الوسط، إذ تُستخدَم نادرًا أكثر من الميزات الموضحة سابقًا في بقية الكتاب ولكنها أكثر شيوعًا من العديد من الميزات المتقدمة الأخرى التي نوقشت في هذا الفصل.

أحد الأمثلة على سمة ذات نوع مرتبط هي سمة Iterator التي توفرها المكتبة القياسية. يُطلق على النوع المرتبط اسم Item ويرمز إلى نوع القيم التي يمرّ عليها النوع الذي ينفّذ سمة Iterator. تُعرّف سمة Iterator كما هو موضح في الشيفرة 12.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

[الشيفرة 12: تعريف سمة Iterator التي لديها نوع مرتبط Item]

النوع Item هو موضع مؤقت، ويوضح تعريف تابع next أنه سيعيد قيمًا من النوع <Option<Self::Item. سيحدد منفّذ سمة Iterator النوع الحقيقي للنوع Item وسيُعيد التابع next النوع Option الذي يحتوي على قيمة من هذا النوع الحقيقي.

قد تبدو الأنواع المرتبطة مثل مفهوم مشابه للأنواع المعممة من حيث أن الأخير يسمح لنا بتعريف دالة دون تحديد الأنواع التي يمكنها التعامل معها، ولفحص الاختلاف بين المفهومين، سنلقي نظرةً على تنفيذ سمة Iterator على نوع يسمى Counter الذي يحدد نوع Item هو u32:

اسم الملف: src/lib.rs

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--

تبدو هذه الصيغة مشابهة لتلك الموجودة في الأنواع المعمّمة generic، فلماذا لا نكتفي بتعريف سمة Iterator باستخدام الأنواع المعممة كما هو موضح في الشيفرة 13؟

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

[الشيفرة 13: تعريف افتراضي لسمة Iterator باستخدام الأنواع المعممة]

الفرق هو أنه علينا أن نوضح الأنواع في كل تنفيذ عند استخدام الأنواع المعمّمة كما في الشيفرة 13، لأنه يمكننا أيضًا تنفيذ Iterator<String> for Counter أو أي نوع آخر، فقد يكون لدينا تنفيذات متعددة للسمة Iterator من أجل Counter. بعبارة أخرى: عندما تحتوي السمة على معامل معمّم، يمكن تنفيذه لنوع ما عدة مرات مع تغيير الأنواع الحقيقية لمعاملات النوع المعمم في كل مرة، وعندما نستخدم التابع next على Counter سيتوجب علينا توفير التعليقات التوضيحية من النوع للإشارة إلى تنفيذ Iterator الذي نريد استخدامه.

لا نحتاج مع الأنواع المرتبطة إلى إضافة تعليقات توضيحية للأنواع، لأننا لا نستطيع تنفيذ سمة على نوع عدة مرات. يمكننا فقط اختيار نوع Item مرةً واحدةً في الشيفرة 12 مع التعريف الذي يستخدم الأنواع المرتبطة لأنه لا يمكن أن يكون هناك سوى impl Iterator for Counter واحد. لا يتعين علينا تحديد أننا نريد مكررًا لقيم u32 في كل مكان نستدعي next على Counter.

تصبح الأنواع المرتبطة أيضًا جزءًا من عقد contract السمة، إذ يجب أن يوفر منفّذو السمة نوعًا لملء الموضع المؤقت للنوع المرتبط. تمتلك الأنواع المرتبطة غالبًا اسمًا يصف كيفية استخدام النوع، كما يُعد توثيق النوع المرتبط في توثيق الواجهة البرمجية ممارسةً جيدة.

معاملات النوع المعمم الافتراضي وزيادة تحميل العامل

عندما نستخدم معاملات النوع المعمم يمكننا تحديد نوع حقيقي افتراضي للنوع المعمم، وهذا يلغي الحاجة إلى منفّذي السمة لتحديد نوع حقيقي إذا كان النوع الافتراضي يعمل. يمكنك تحديد نوع افتراضي عند التصريح عن نوع عام باستخدام الصيغة <PlaceholderType=ConcreteType>.

من الأمثلة الرائعة على الموقف الذي تكون فيه هذه التقنية مفيدة هو زيادة تحميل العامل operator overloading، إذ يمكنك تخصيص سلوك عامل (مثل +) في مواقف معينة.

لا تسمح لك رست بإنشاء عواملك الخاصة أو زيادة تحميل العوامل العشوائية، ولكن يمكنك زيادة تحميل العمليات والسمات المقابلة المدرجة في std::ops من خلال تنفيذ السمات المرتبطة بالعامل. على سبيل المثال في الشيفرة 14 زدنا تحميل العامل + لإضافة نسختين Point معًا عن طريق تنفيذ سمة Add على بنية Point.

اسم الملف: src/main.rs

use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

[الشيفرة 14: تنفيذ سمة Add لزيادة تحميل العامل + لنسخ Point]

يضيف التابع add قيم x لنسختَي Point وقيم y لنسختَي Point لإنشاء Point جديدة. للسمة Add نوع مرتبط يسمى Output الذي يحدد النوع الذي يُعاد من التابع add.

النوع المعمم الافتراضي في هذه الشيفرة موجودٌ ضمن سمة Add. إليك تعريفه:

trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}

يجب أن تبدو هذه الشيفرة مألوفة عمومًا: سمة ذات تابع واحد ونوع مرتبط بها. الجزء الجديد هو Rhs=Self، إذ يُطلق على الصيغة هذه معاملات النوع الافتراضية default type parameters. يعرّف معامل النوع المعمم Rhs (اختصار للجانب الأيمن right hand size) نوع المعامل rhs في تابع add. إذا لم نحدد نوعًا حقيقيًا للقيمة Rhs عند تنفيذ سمة Add، سيُعيّن نوع Rhs افتراضيًا إلى Self الذي سيكون النوع الذي ننفّذ السمة Add عليه.

عندما نفّذنا Add على Point استخدمنا الإعداد الافتراضي للنوع Rhs لأننا أردنا إضافة نسختين Point. لنلقي نظرةً على مثال لتنفيذ سمة Add، إذ نريد تخصيص نوع Rhs بدلًا من الاستخدام الافتراضي.

لدينا الهيكلان Millimeters و Meters اللذان يحملان قيمًا في وحدات مختلفة، يُعرف هذا الغلاف الرقيق لنوع موجود في هيكل آخر باسم نمط النوع الجديد newtype pattern الذي نصفه بمزيد من التفصيل لاحقًا في قسم "استخدام نمط النوع الجديد لتنفيذ السمات الخارجية على الأنواع الخارجية". نريد أن نضيف القيم بالمليمترات إلى القيم بالأمتار وأن نجعل تنفيذ Add يجري التحويل صحيحًا. يمكننا تنفيذ Add للنوع Millimeters مع Meters مثل Rhs كما هو موضح في الشيفرة 15.

اسم الملف: src/lib.rs

use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

[الشيفرة 15: تنفيذ سمة Add على Millimeters لإضافة Millimeters للنوع Meters]

لإضافة Millimeters و Meters نحدد <impl Add<Meters لتعيين قيمة محدد نوع Rhs بدلًا من استخدام الافتراضي Self.

ستستخدم معاملات النوع الافتراضية بطريقتين رئيسيتين:

  • لتوسيع نوع دون تعطيل الشيفرة الموجودة.
  • للسماح بالتخصيص في حالات معينة لن يحتاجها معظم المستخدمين.

تعد سمة المكتبة القياسية Add مثالًا على الغرض الثاني: ستضيف عادةً نوعين متشابهين، بينما توفّر خاصية Add القدرة على التخصيص بعد ذلك. يعني استخدام معامل النوع الافتراضي في تعريف سمة Add أنك لست مضطرًا لتحديد المعامل الإضافي في معظم الأوقات. بعبارة أخرى ليست هناك حاجة إلى القليل من التنفيذ المعياري، وهذا ما يسهل استخدام السمة.

صيغة مؤهلة كليا للتوضيح باستدعاء التوابع التي تحمل الاسم ذاته

لا شيء في رست يمنع سمةً ما من أن يكون لها تابع يحمل اسم تابع السمة الأخرى ذاتها، كما لا تمنعك رست من تنفيذ كلتا السمتين على نوع واحد، ومن الممكن أيضًا تنفيذ تابع مباشرةً على النوع الذي يحمل اسم التوابع نفسه من السمات.

عند استدعاء التوابع التي تحمل الاسم نفسه، ستحتاج إلى إخبار رست بالتابع الذي تريد استخدامه. ألقِ نظرةً على الشيفرة 16، إذ عرّفنا سمتين Pilot و Wizard ولكل منهما تابع يسمى fly، ثم نفّذنا كلتا السمتين على نوع Human لديه فعلًا تابع يسمى fly منفّذ عليه، وكل تابع fly يفعل شيئًا مختلفًا.

اسم الملف: src/main.rs

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

[الشيفرة 16: تعريف سمتين بحيث تملكان تابع fly وتنفيذهما على النوع Human وتنفيذ تابع fly على Human مباشرةً]

عندما نستدعي fly على نسخة Human يتخلف المصرف عن استدعاء التابع الذي يُنفّذ مباشرةً على النوع كما هو موضح في الشيفرة 17.

اسم الملف: src/main.rs

fn main() {
    let person = Human;
    person.fly();
}

[الشيفرة 17: استدعاء fly على نسخة Human]

سيؤدي تنفيذ هذه الشيفرة إلى طباعة *waving arms furiously* مما يدل على أن رست استدعت تابع fly المنفّذ على Human مباشرةً.

نحتاج إلى استخدام صيغة أكثر وضوحًا عند استدعاء توابع fly من سمة Pilot أو Wizard لتحديد تابع fly الذي نعنيه، وتوضّح الشيفرة 18 هذه الصيغة.

اسم الملف: src/main.rs

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

[الشيفرة 18: تحديد تابع السمة fly التي نريد استدعاءه]

يوضح تحديد اسم السمة قبل اسم التابع لرست أي تنفيذ للتابع fly نريد استدعاءه. يمكننا أيضًا كتابة Human::fly(&person)‎ وهو ما يعادل person.fly()‎الذي استخدمناه في الشيفرة 18، ولكن هذا أطول قليلًا للكتابة إذا لم نكن بحاجة إلى توضيح.

يؤدي تنفيذ هذه الشيفرة إلى طباعة التالي:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

إذا كان لدينا نوعان ينفذ كلاهما سمةً واحدةً، يمكن أن تكتشف رست أي تنفيذ للسمة يجب استخدامه بناءً على نوع self نظرًا لأن تابع fly يأخذ معامل self، ومع ذلك فإن الدوال functions المرتبطة التي ليست بتوابع لا تحتوي على معامل self. عندما تكون هناك أنواع أو سمات متعددة تحدد دوالًا غير تابعية non-method بنفس اسم الدالة، لا تعرف رست دائمًا النوع الذي تقصده ما لم تستخدم صيغة مؤهلة كليًا fully qualified syntax. على سبيل المثال في الشيفرة 19 أنشأنا سمة لمأوى للحيوانات يسمّي جميع الجراء puppies الصغار Spot. ننشئ سمة Animal بدالة غير تابعية مرتبطة baby_name، تُنفّذ Animal لهيكل Dog الذي نوفّر عليه أيضًا دالةً غير تابعية مرتبطة baby_name مباشرةً.

اسم الملف: src/main.rs

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}

[الشيفرة 19: سمة لها دالة مرتبطة ونوع له دالة مرتبطة بالاسم ذاته الذي ينفّذ السمة أيضًا]

ننفّذ الشيفرة الخاص بتسمية كل الجراء في الدالة المرتبطة baby_name والمُعرّفة في Dog. ينفّذ النوع Dog أيضًا سمة Animal التي تصف الخصائص التي تمتلكها جميع الحيوانات. يُطلق على أطفال الكلاب اسم الجراء ويُعبّر عن ذلك في تنفيذ سمة Animal على Dog في الدالة المرتبطة baby_name بالسمة Animal

نستدعي في الدالة main الدالة Dog::baby_name التي تستدعي الدالة المرتبطة المعرفة في Dog مباشرةً. تطبع هذه الشيفرة ما يلي:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

لم نكن نتوقع هذه النتيجة، إذ نريد استدعاء دالة baby_name التي تعد جزءًا من سمة Animal التي نفّذناها على Dog حتى تطبع الشيفرة A baby dog is called a puppy. لا تساعد تقنية تحديد اسم السمة التي استخدمناها في الشيفرة 18 هنا، وإذا غيرنا main للشيفرة لما هو موجود في الشيفرة 20، سنحصل على خطأ عند التصريف.

اسم الملف: src/main.rs

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

[الشيفرة 20: محاولة استدعاء الدالة baby_name من السمة Animal دون معرفة رست بأي تنفيذ ينبغي استخدامه]

لا تستطيع رست معرفة أي تنفيذ نريده للقيمة Animal::baby_name لأن Animal::baby_name لا تحتوي على معامل self ولأنه يمكن أن تكون هناك أنواع أخرى تنفّذ سمة Animal. سنحصل على خطأ المصرف هذا:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
2  |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
   |                                           +++++++       +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` due to previous error

لإزالة الغموض وإخبار رست أننا نريد استخدام تنفيذ Animal للقيمة Dog بدلًا من استخدام Animal لبعض الأنواع الأخرى، نحتاج إلى استخدام صيغة مؤهلة كليًا. توضح الشيفرة 21 كيفية استخدام صيغة مؤهلة كليًا.

اسم الملف: src/main.rs

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

[الشيفرة 21: استعمال صيغة مؤهلة كليًا لتحديد أننا نريد استدعاء دالة baby_name من السمة Animal كما هو منفّذ في Dog]

نقدم لرست تعليقًا توضيحيًا للنوع ضمن أقواس مثلثية angle brackets مما يشير إلى أننا نريد استدعاء تابع baby_name من سمة Animal كما هو منفّذ في Dog بالقول إننا نريد معاملة النوع Dog مثل النوع Animal لاستدعاء الدالة هذه. ستطبع الشيفرة البرمجية الآن ما نريد:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

تعرف الصيغة المؤهلة كليًا عمومًا على النحو التالي:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

بالنسبة للدوال المرتبطة التي ليست توابع، لن يكون هناك receiver، سيكون هناك فقط قائمة من الوسائط الأخرى. يمكنك استخدام صيغة مؤهلة كليًا في كل مكان تستدعي فيه دوالًا أو توابعًا، ومع ذلك يُسمح لك بحذف أي جزء من هذه الصيغة التي يمكن لرست اكتشافها من المعلومات الأخرى في البرنامج. ما عليك سوى استخدام هذه الصيغة المطولة أكثر في الحالات التي توجد فيها العديد من التنفيذات التي تستخدم الاسم ذاته وتحتاج رست إلى المساعدة في تحديد التنفيذ الذي تريد استدعاءه.

استخدام سمات خارقة supertrait لطلب وظيفة إحدى السمات ضمن سمة أخرى

في بعض الأحيان قد تكتب تعريف سمة يعتمد على سمة أخرى: لكي ينفّذ النوع السمة الأولى فأنت تريد أن تطلب هذا النوع لتنفيذ السمة الثانية أيضًا. يمكنك فعل ذلك حتى يتمكن تعريف السمة الخاص بك من الاستفادة من العناصر المرتبطة للسمة الثانية. تسمى السمة التي يعتمد عليها تعريف السمة الخاص بك بالسمة الخارقة لسمتك.

على سبيل المثال لنفترض أننا نريد إنشاء سمة OutlinePrint باستخدام تابع outline_print الذي سيطبع قيمة معينة منسقة بحيث تكون مؤطرة بعلامات نجمية. بالنظر إلى هيكل Point الذي ينفّذ سمة المكتبة القياسية Display لتعطي النتيجة (x, y)، عندما نستدعي outline_print على نسخة Point التي تحتوي على 1 للقيمة x و 3 للقيمة y، يجب أن يطبع ما يلي:

**********
*        *
* (1, 3) *
*        *
**********

نريد استخدام وظيفة سمة Display في تنفيذ طريقة outline_print، لذلك نحتاج إلى تحديد أن سمة OutlinePrint ستعمل فقط مع الأنواع التي تنفّذ أيضًا Display وتوفر الوظائف التي تحتاجها OutlinePrint. يمكننا فعل ذلك في تعريف السمة عن طريق تحديد OutlinePrint: Display، وتشبه هذه التقنية إضافة سمة مرتبطة بالسمة. تُظهر الشيفرة 22 تنفيذ سمة OutlinePrint.

اسم الملف: src/main.rs

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

[الشيفرة 22: تنفيذ سمة OutlinePrint التي تتطلب الوظيفة من Display]

بما أننا حددنا أن OutlinePrint تتطلب سمة Display، يمكننا استخدام دالة to_string التي تُنفّذ تلقائيًا لأي نوع ينفّذ Display. إذا حاولنا استخدام to_string دون إضافة نقطتين وتحديد سمة Display بعد اسم السمة، سنحصل على خطأ يقول بأنه لم يُعثر على تابع باسم to_string للنوع Self& في النطاق الحالي.

لنرى ما يحدث عندما نحاول تنفيذ OutlinePrint على نوع لا ينفّذ Display مثل هيكل Point.

اسم الملف: src/main.rs

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

نحصل على خطأ يقول أن Display مطلوبة ولكن غير منفّذة:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:6
   |
20 | impl OutlinePrint for Point {}
   |      ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
3  | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` due to previous error

لإصلاح هذا الأمر ننفّذ Display على Point ونلبي القيد الذي تتطلبه OutlinePrint مثل:

اسم الملف: src/main.rs

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

سيُصرَّف تنفيذ سمة OutlinePrint على Point بعدها بنجاح، ويمكننا استدعاء outline_print على نسخة Point لعرضها ضمن مخطط من العلامات النجمية.

استخدام نمط النوع الجديد لتنفيذ سمات الخارجية على الأنواع الخارجية

ذكرنا سابقًا في الفصل السمات Traits في لغة رست في قسم "تطبيق السمة على نوع" القاعدة الوحيدة التي تنص على أنه لا يُسمح لنا إلا بتنفيذ سمة على نوع ما إذا كانت السمة أو النوع محليين بالنسبة للوحدة المصرفة الخاصة بنا، إلا أنه من الممكن التحايل على هذا القيد باستخدام نمط النوع الجديد Newtype pattern الذي يتضمن إنشاء نوع جديد في هيكل الصف (ناقشنا هياكل الصف سابقًا في قسم "استخدام هياكل الصفوف دون حقول مسماة لإنشاء أنواع مختلفة" من الفصل استخدام الهياكل structs لتنظيم البيانات في لغة رست)، إذ سيكون لهيكل الصف حقلًا واحدًا وسيكون هناك غلافًا رفيعًا حول النوع الذي نريد تنفيذ سمة له، ثم يكون نوع الغلاف محليًا بالنسبة للوحدة المصرفة الخاصة بنا ويمكننا تنفيذ السمة على الغلاف. النوع الجديد هو مصطلح ينشأ من لغة البرمجة هاسكل Haskell. لا يوجد تأثير سلبي على وقت التنفيذ لاستخدام هذا النمط ويُستبعد نوع الغلاف في وقت التصريف.

على سبيل المثال، لنفترض أننا نريد تنفيذ Display على <Vec<T التي تمنعنا القاعدة الوحيدة من فعل ذلك مباشرةً لأن سمة Display ونوع <Vec<T مُعرّفان خارج الوحدة المصرفة الخاصة بنا. يمكننا عمل هيكل Wrapper يحتوي على نسخة من <Vec<T ومن ثم تنفيذ Display على Wrapper واستخدام قيمة <Vec<T كما هو موضح في الشيفرة 23.

اسم الملف: src/main.rs

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

[الشيفرة 23: إنشاء نوع Wrapper حول <Vec<String لتنفيذ Display]

تُستخدم self.0 من قِبل تنفيذ Display للوصول إلى <Vec<T الداخلي، لأن Wrapper هيكل صف و <Vec<T هو العنصر في الدليل 0 في الصف. يمكننا بعد ذلك استخدام وظيفة نوع Display على Wrapper.

الجانب السلبي لاستخدام هذه التقنية هو أن Wrapper هو نوع جديد لذلك لا يحتوي على توابع القيمة التي يحملها. سيتوجب علينا تنفيذ جميع توابع <Vec<T مباشرةً على Wrapper، بحيث تفوض هذه التوابع إلى self.0، ليسمح لنا بالتعامل مع Wrapper تمامًا مثل <Vec<T.

إذا أردنا أن يحتوي النوع الجديد على كل تابع يمتلكه النوع الداخلي، سيكون تنفيذ سمة Deref (ناقشناها سابقًا في الفصل معاملة المؤشرات الذكية Smart Pointers مثل مراجع نمطية Regular References باستخدام سمة Deref في لغة رست) على Wrapper لإرجاع النوع الداخلي حلًا مناسبًا. إذا كنا لا نريد أن يحتوي نوع Wrapper على جميع التوابع من النوع الداخلي -على سبيل المثال لتقييد سلوك نوع Wrapper- سيتعين علينا تنفيذ التوابع التي نريدها يدويًا فقط.

نموذج النمط الجديد هذا مفيد أيضًا حتى عندما لا تكون السمات متضمنة. لنغير تركيزنا الآن، ونلقي نظرةً على بعض الطرق المتقدمة للتفاعل مع نظام نوع رست.

ترجمة -وبتصرف- لقسم من الفصل Advanced Features من كتاب 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.


×
×
  • أضف...