الإرسال الديناميكي (Dynamic Dispatch)
جزء من قسم Object Oriented Programming في رحلة Rust على Coddy — الدرس 45 من 61.
عندما تستخدم الأنواع العامة (generics) مع قيود السمات (trait bounds) مثل <T: Summary>، يحدد Rust النوع الدقيق في وقت التجميع (compile time). يُسمى هذا الإرسال الثابت (static dispatch)—حيث يقوم المترجم (compiler) بإنشاء كود متخصص لكل نوع ملموس (concrete type) تستخدمه. إنه سريع، ولكن هناك قيد: يمكن للمتغير أن يحمل نوعاً واحداً محدداً فقط.
ماذا لو كنت بحاجة إلى متغير واحد يمكنه الاحتفاظ بأنواع مختلفة في وقت التشغيل؟ هنا يأتي دور كائنات السمات (trait objects). باستخدام Box<dyn Trait>، يمكنك تخزين أي نوع يقوم بتنفيذ هذه السمة:
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 animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.speak()); // "Woof!"
let animal: Box<dyn Speak> = Box::new(Cat);
println!("{}", animal.speak()); // "Meow!"
}
تشير الكلمة المفتاحية dyn إلى الإرسال الديناميكي (dynamic dispatch)—حيث تبحث لغة Rust عن الطريقة التي سيتم استدعاؤها في وقت التشغيل بدلاً من وقت التجميع. يعد Box ضرورياً لأن كائنات السمات (trait objects) ليس لها حجم معروف؛ حيث يوفر Box مؤشراً بحجم ثابت.
فكر في الأمر بهذه الطريقة: تقول القوالب العامة (generics) "أنا أعمل مع النوع T"، بينما تقول كائنات السمات (trait objects) "أنا أعمل مع أي شيء يمكنه القيام بذلك". المقايضة هي تكلفة بسيطة في وقت التشغيل مقابل المرونة في التعامل مع أنواع مختلفة من خلال نفس المتغير.
التحدي
سهلدعنا نبني نظاماً لتأجير المركبات يوضح قوة الإرسال الديناميكي (dynamic dispatch)! ستقوم بإنشاء سمة (trait) تحدد ما يمكن لأي مركبة قابلة للتأجير القيام به، ثم تستخدم Box<dyn Trait> لتخزين أنواع مختلفة من المركبات في نفس المتغير.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
vehicle.rs: قم بتعريف سمة عامةRentableمع طريقةrental_infoتأخذ&selfوتعيدString. ثم أنشئ هيكلين (structs) عامين:Car— مع حقل عامmodelمن نوع (String). يجب أن تعيدrental_infoالخاصة به"Car: {model}"Bike— مع حقل عامbrandمن نوع (String). يجب أن تعيدrental_infoالخاصة به"Bike: {brand}"
main.rs: قم باستيراد وحدة المركبات (vehicle module) واستخدم المدخلات لتوضيح الإرسال الديناميكي. أنشئ متغيراً من نوعBox<dyn Rentable>يحمل أولاًCar، واطبع معلومات الإيجار الخاصة به، ثم أعد تعيين نفس المتغير ليحملBikeواطبع معلومات الإيجار الخاصة به مرة أخرى.
السحر هنا هو أن متغيراً واحداً من نوع Box<dyn Rentable> يمكنه حمل إما Car أو Bike—حيث يحدد Rust أي طريقة rental_info سيتم استدعاؤها في وقت التشغيل (runtime). هذا هو الإرسال الديناميكي في أبهى صوره!
يجب أن يظهر مخرجك معلومات كلتا المركبتين:
Car: {model}
Bike: {brand}على سبيل المثال، مع المدخلات "Tesla Model 3" و "Trek":
Car: Tesla Model 3
Bike: Trekستتلقى مدخلين: طراز السيارة (car model) وعلامة الدراجة التجارية (bike brand).
ورقة مرجعية
تستخدم لغة Rust **الإرسال الساكن (static dispatch)** مع القوالب العامة (generics)—حيث يقوم المترجم بإنشاء كود مخصص لكل نوع في وقت الترجمة. هذا سريع ولكنه يعني أن المتغير يمكنه حمل نوع واحد محدد فقط.
تمكن **كائنات السمات (Trait objects)** من **الإرسال الديناميكي (dynamic dispatch)**، مما يسمح لمتغير واحد بحمل أنواع مختلفة في وقت التشغيل. استخدم Box<dyn Trait> لتخزين أي نوع يطبق هذه السمة:
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 animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.speak()); // "Woof!"
let animal: Box<dyn Speak> = Box::new(Cat);
println!("{}", animal.speak()); // "Meow!"
}تشير الكلمة المفتاحية dyn إلى الإرسال الديناميكي—حيث يتم حل استدعاءات الدوال في وقت التشغيل. يعد Box مطلوباً لأن كائنات السمات ليس لها حجم معروف في وقت الترجمة.
الفرق الجوهري: تقول القوالب العامة (Generics) "أنا أعمل مع النوع T" (نوع واحد محدد)، بينما تقول كائنات السمات "أنا أعمل مع أي شيء يطبق هذه السمة" (أنواع متعددة من خلال نفس المتغير). المقايضة هي تكلفة بسيطة في وقت التشغيل مقابل المرونة.
جرّب بنفسك
mod vehicle;
use vehicle::{Rentable, Car, Bike};
fn main() {
// قراءة المدخلات
let mut car_model = String::new();
std::io::stdin().read_line(&mut car_model).expect("Failed to read line");
let car_model = car_model.trim().to_string();
let mut bike_brand = String::new();
std::io::stdin().read_line(&mut bike_brand).expect("Failed to read line");
let bike_brand = bike_brand.trim().to_string();
// TODO: يحمل سيارة Box<dyn Rentable> قم بإنشاء متغير من نوع
// rental_info() اطبع معلومات الإيجار الخاصة به باستخدام الدالة
// TODO: أعد تعيين نفس المتغير ليحمل دراجة
// اطبع معلومات الإيجار الخاصة به مرة أخرى
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس 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مراجعة - مدير حديقة الحيوان