مقدمه

در فصل دهم، دیدیم که چگونه می‌توانیم از trait boundها و نوع‌های جنریک برای نوشتن کدی استفاده کنیم که روی نوع‌های مختلفی که یک trait خاص را پیاده‌سازی کرده‌اند، کار کند. این یک نوع «پلی‌مورفیسم استاتیک» است، زیرا کامپایلر Rust در زمان کامپایل، کد جنریک را با کد تخصصی برای هر نوع مشخص، جایگزین می‌کند (فرآیندی به نام monomorphization).

اما گاهی اوقات ما به انعطاف‌پذیری بیشتری نیاز داریم. فرض کنید می‌خواهیم یک کالکشن (مانند یک وکتور) داشته باشیم که مقادیری از نوع‌های مختلف را در خود نگه دارد، با این شرط که تمام آن نوع‌ها یک رفتار مشترک (یک trait مشترک) را پیاده‌سازی کنند. اینجاست که «اشیاء تریت» یا Trait Objects وارد عمل می‌شوند و به ما اجازه می‌دهند تا «پلی‌مورفیسم دینامیک» را پیاده‌سازی کنیم.

تعریف یک Trait Object

یک trait object به یک نمونه از یک trait اشاره می‌کند. ما با استفاده از یک اشاره‌گر (مانند & یا Box) و کلمه کلیدی dyn قبل از نام trait، یک trait object می‌سازیم. برای مثال، &dyn Draw یک رفرنس به هر نوعی است که Draw را پیاده‌سازی کرده باشد.

بیایید یک مثال را بررسی کنیم. فرض کنید یک کتابخانه رابط کاربری گرافیکی (GUI) می‌سازیم که می‌تواند مجموعه‌ای از کامپوننت‌های قابل ترسیم را رندر کند. هر کامپوننت نوع خاص خود را دارد (مانند Button یا TextField)، اما همگی یک رفتار مشترک draw را دارند.

Copy Icon src/lib.rs
pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    // This vector holds trait objects of any type that implements the Draw trait.
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

در اینجا، Screen یک وکتور به نام components دارد. نوع این وکتور Vec<Box<dyn Draw>> است. این یعنی ما یک وکتور از `Box`ها داریم که هر Box به داده‌ای روی heap اشاره می‌کند که Draw را پیاده‌سازی کرده است. ما نمی‌دانیم نوع دقیق آن داده چیست (می‌تواند Button یا TextField باشد)، اما کامپایلر می‌داند که هر نوعی که باشد، حتماً متد draw را دارد.

پیاده‌سازی Trait برای نوع‌های مختلف

حالا بیایید چند کامپوننت مشخص بسازیم که همگی Draw را پیاده‌سازی کنند.

Copy Icon src/lib.rs
pub struct Button { /* fields */ }
impl Draw for Button {
    fn draw(&self) { println!("Drawing a button"); }
}

pub struct SelectBox { /* fields */ }
impl Draw for SelectBox {
    fn draw(&self) { println!("Drawing a select box"); }
}

استفاده از Trait Objects

اکنون در crate باینری خود، می‌توانیم نمونه‌هایی از Screen بسازیم و کامپوننت‌هایی از نوع‌های مختلف را به آن اضافه کنیم.

Copy Icon src/main.rs
use gui::{Draw, Button, Screen, SelectBox};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox { /* ... */ }),
            Box::new(Button { /* ... */ }),
        ],
    };

    screen.run();
}

وقتی متد screen.run() فراخوانی می‌شود، Rust در زمان اجرا نوع هر trait object را بررسی نمی‌کند. بلکه از طریق اشاره‌گرهای داخل trait object، متد draw صحیح را برای هر کامپوننت فراخوانی می‌کند. این کار با هزینه‌ی کمی در زمان اجرا همراه است (به دلیل نیاز به dereference کردن اشاره‌گر)، اما انعطاف‌پذیری فوق‌العاده‌ای را فراهم می‌کند.

یک محدودیت برای trait objectها این است که trait مورد نظر باید «ایمن برای شیء» (object-safe) باشد. دو قانون اصلی برای این موضوع وجود دارد: نوع بازگشتی تمام متدها نباید Self باشد و هیچ‌کدام از متدها نباید پارامترهای نوع جنریک داشته باشند.

در این درس با trait objectها به عنوان روش Rust برای پیاده‌سازی پلی‌مورفیسم دینامیک آشنا شدیم. دیدیم که چگونه می‌توان با استفاده از Box<dyn Trait>، مجموعه‌ای از اشیاء با نوع‌های مختلف که یک رفتار مشترک دارند را مدیریت کرد. در درس بعدی، این مفاهیم را در قالب «پیاده‌سازی یک الگوی شی‌گرا» برای وضعیت‌های یک پست وبلاگ به کار خواهیم بست.