متجهات من الـ Traits
جزء من قسم Object Oriented Programming في رحلة Rust على Coddy — الدرس 46 من 61.
لقد رأيت كيف يسمح Box<dyn Trait> لمتغير واحد بحمل أنواع مختلفة. تظهر القوة الحقيقية عندما تجمع بين هذا والمجموعات (collections) — تخزين أنواع مختلفة متعددة في متجه (vector) واحد.
يتطلب المتجه (vector) العادي أن تكون جميع العناصر من نفس النوع. لا يمكنك امتلاك Vec<Dog> يحتوي أيضاً على قطط. ولكن مع كائنات السمات (trait objects)، يمكنك إنشاء متجه يحمل أي شيء يقوم بتطبيق سمة مشتركة:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
struct Bird;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
impl Speak for Bird {
fn speak(&self) -> String { String::from("Tweet!") }
}
fn main() {
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog),
Box::new(Cat),
Box::new(Bird),
];
}
النوع Vec<Box<dyn Speak>> يعني "متجه من كائنات السمات المغلفة (boxed trait objects)". كل عنصر هو Box يشير إلى شيء ينفذ Speak—الأنواع الفعلية يمكن أن تختلف. تقوم بتغليف كل قيمة بـ Box::new() عند إضافتها إلى المتجه.
هذا النمط ضروري لبناء أنظمة مرنة حيث تحتاج إلى إدارة مجموعات من الكائنات المرتبطة ولكن المختلفة—مثل مكونات واجهة المستخدم (UI components)، أو كيانات الألعاب (game entities)، أو أنظمة الإضافات (plugin systems).
التحدي
سهللنقم ببناء نظام إشعارات يمكنه التعامل مع أنواع مختلفة من التنبيهات! ستقوم بإنشاء مجموعة تخزن أنواعًا مختلفة من الإشعارات — رسائل البريد الإلكتروني، ورسائل SMS، وإشعارات الدفع (push notifications) — جميعها في متجه (vector) واحد باستخدام كائنات السمة (trait objects).
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
notifications.rs: قم بتعريف سمة (trait) عامة باسمNotifyمع طريقةsendتأخذ&selfوتعيدStringيصف الإشعار الذي يتم إرساله. ثم أنشئ ثلاث هياكل (structs) عامة:Email— مع حقل عامrecipientمن نوع (String). يجب أن تعيد طريقةsendالخاصة به"Email to: {recipient}"Sms— مع حقل عامphoneمن نوع (String). يجب أن تعيد طريقةsendالخاصة به"SMS to: {phone}"Push— مع حقل عامdeviceمن نوع (String). يجب أن تعيد طريقةsendالخاصة به"Push to: {device}"
main.rs: قم باستيراد وحدة الإشعارات الخاصة بك وأنشئ متجهًا من نوعVec<Box<dyn Notify>>يحتوي على جميع أنواع الإشعارات الثلاثة. استخدم المدخلات المقدمة لإنشاء واحد من كل نوع إشعار، وأضفهم إلى المتجه الخاص بك بالترتيب (Email، Sms، Push)، ثم اطبع نتيجة استدعاءsend()على العنصر الأول في المتجه.
المفهوم الأساسي هنا هو أنه على الرغم من أن Email و Sms و Push هي هياكل مختلفة تمامًا، إلا أنها يمكن أن تتواجد معًا في نفس المتجه لأنها تشترك في سمة Notify. يتم تغليف كل عنصر بـ Box::new() لإنشاء كائن السمة.
يجب أن يظهر مخرجك الإشعار الأول:
Email to: {recipient}على سبيل المثال، مع المدخلات "alice@example.com" و "555-1234" و "iPhone-12":
Email to: alice@example.comستتلقى ثلاثة مدخلات: مستلم البريد الإلكتروني، ورقم الهاتف، واسم الجهاز.
ورقة مرجعية
يسمح Vec<Box<dyn Trait>> بتخزين أنواع مختلفة متعددة في متجه (vector) واحد، طالما أنها جميعاً تطبق نفس السمة (trait).
تتطلب المتجهات العادية أن تكون جميع العناصر من نفس النوع، ولكن كائنات السمات (trait objects) تُمكّن من إنشاء مجموعات غير متجانسة:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
fn main() {
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog),
Box::new(Cat),
];
}
يتم تغليف كل عنصر بـ Box::new() عند إضافته إلى المتجه. يمكن أن تختلف الأنواع الفعلية طالما أنها تطبق السمة المشتركة.
جرّب بنفسك
mod notifications;
use notifications::{Notify, Email, Sms, Push};
fn main() {
// قراءة المدخلات
let mut recipient = String::new();
std::io::stdin().read_line(&mut recipient).expect("Failed to read line");
let recipient = recipient.trim().to_string();
let mut phone = String::new();
std::io::stdin().read_line(&mut phone).expect("Failed to read line");
let phone = phone.trim().to_string();
let mut device = String::new();
std::io::stdin().read_line(&mut device).expect("Failed to read line");
let device = device.trim().to_string();
// TODO: لتمثيل أنواع إشعارات مختلفة Vec<Box<dyn Notify>> أنشئ
// TODO: باستخدام المدخلات Push و Sms و Email أنشئ كائنات من
// TODO: Box::new() أضفهم إلى المتجه بالترتيب (Email, Sms, Push) باستخدام
// TODO: اطبع نتيجة استدعاء ()send على العنصر الأول
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1الأساليب والسلوك
مقدمة في كتل التنفيذمعامل Selfالأساليب القابلة للتعديلالدوال المرتبطةكتل تنفيذ متعددةتسلسل الأساليبمراجعة - إجراءات المستطيل4مشروع: حيوان أليف افتراضي
تعريف الحيوان الأليفإطعام الحيوان الأليف2التغليف والوحدات البرمجية
أساسيات الوحدات البرمجيةالكلمة المفتاحية Publicالحقول الخاصةدوال Gettersدوال Settersمراجعة - الخزانة الآمنة5الأنواع العامة (Generics)
الـ Structs العامةالـ Methods العامةأنواع عامة متعددةالـ Functions العامةمراجعة - Coordinate Point3التعدادات المتقدمة (Advanced Enums)
التعدادات مع البياناتالأساليب (Methods) في التعداداتمطابقة حالات البياناتمراجعة التعداد Optionملخص - التعداد Shape6تعريف الـ Traits
ما هي الـ Trait؟تنفيذ الـ Traitsالتنفيذات الافتراضيةتجاوز التنفيذات الافتراضيةالـ Traits مع المعاملاتملخص - مشغل الوسائط9كائنات Trait
الإرسال الديناميكي (Dynamic Dispatch)متجهات من الـ Traitsالتكرار عبر كائنات Traitمراجعة - مدير حديقة الحيوان