পাঠ ১৮.২

Trait Object দিয়ে shared behavior abstract করা

Using Trait Objects That Allow for Values of Different Types

কখনো এমন collection দরকার হয় যাতে একই trait implement করা একাধিক আলাদা concrete type একসাথে রাখা যায়। Rust-এ এই কাজে আসে trait objectdyn Trait syntax-এ লেখা একটা pointer, যেটা runtime-এ method dispatch করে।

সমস্যা — Vec একটাই type রাখতে পারে

সাধারণ Vec<T> একটাই concrete type রাখতে পারে। Enum দিয়ে fixed কয়েকটা type-এর জন্য workaround করা যায় (যেমন SpreadsheetCell), কিন্তু library author চাইতে পারে user নিজের নতুন type যোগ করুক — যেগুলো compile-time-এ unknown। এই problem-এর সমাধান trait object।

Common behavior-এর জন্য trait define

ধরো একটা GUI library বানাচ্ছি, যেখানে অনেক ধরনের component থাকবে — সবাই নিজেকে draw করতে জানে।

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

Trait object দিয়ে Screen

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

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

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

Vec<Box<dyn Draw>> — এই vector-এ একাধিক ভিন্ন concrete type রাখা যাবে, যত যেগুলোই Draw implement করবে। Trait object একটা pointer — point করে dual করে: instance-এর data, এবং vtable (virtual method table) যেটা runtime-এ method lookup করে।

Generic-এর সাথে পার্থক্য

Generic + trait bound দিয়ে একই কাজ লেখা যায়, কিন্তু আচরণ ভিন্ন:

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}

impl<T> Screen<T>
where
    T: Draw,
{
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

এখানে Screen<T>-এর প্রতিটা instance একটাই concrete type ধরে রাখতে পারবে — হয় শুধু Button, অথবা শুধু TextField; mix না।

সারসংক্ষেপ:

  • Generic — monomorphization, static dispatch, fast, কিন্তু এক instance-এ এক type।
  • Trait object — dynamic dispatch (vtable lookup), inlining সম্ভব না, একটু runtime overhead, কিন্তু এক collection-এ heterogeneous type।

Trait implement

src/lib.rsrust
pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // code to actually draw a button
    }
}

User-defined type — library-র বাইরে থেকে যোগ করা:

src/main.rsrust
use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

Mixed collection ব্যবহার

src/main.rsrust
use gui::{Button, Screen};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No"),
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

run method-কে concrete type জানতে হয় না — সে শুধু জানে প্রতিটা component Draw implement করে, তাই draw() call করা safe।

Duck typing — কিন্তু compile-time safe

এটা dynamically-typed language-এর "duck typing"-এর মতো — If it walks like a duck and quacks like a duck, then it must be a duck! কিন্তু Rust compile-time-এ check করে। Trait implement না করা type collection-এ ঢোকালে error:

use gui::Screen;

fn main() {
    let screen = Screen {
        components: vec![Box::new(String::from("Hi"))],
    };

    screen.run();
}
compile errortext
error[E0277]: the trait bound `String: Draw` is not satisfied
 --> src/main.rs:5:26
  |
5 |         components: vec![Box::new(String::from("Hi"))],
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`

Dynamic language-এ এই ভুল production-এ runtime exception হয়ে আঘাত করে। Rust আগে থেকে আটকায় — flexibility সাথে safety।

Static vs dynamic dispatch

  • Static dispatch (generic) — compiler প্রতিটা concrete type-এর জন্য আলাদা code generate করে (monomorphization)। Inlining আর full optimization সম্ভব। Runtime-এ কোনো extra cost নেই।
  • Dynamic dispatch (trait object) — compiler emit করা code runtime-এ vtable pointer follow করে method খুঁজে বের করে। Inlining আটকায়, performance overhead সামান্য, কিন্তু flexibility বেশি।

Object safety / dyn compatibility

সব trait dyn-এর সাথে use করা যায় না — কিছু rule আছে যেগুলো "dyn compatibility" বলে। বিস্তারিত Rust reference-এর object safety section-এ। আপাতত মনে রাখো — কিছু trait method (যেমন Self return করা, generic method-যুক্ত) object-safe না।

এই পাঠ থেকে যা শিখলে

  • Trait object — Box<dyn Trait> বা &dyn Trait — pointer + vtable; runtime dispatch।
  • Generic এক instance-এ এক type রাখে; trait object একই collection-এ ভিন্ন ভিন্ন type রাখতে দেয়।
  • Static dispatch দ্রুত (monomorphization, inlining), dynamic dispatch flexible (heterogeneous collection)।
  • Trait implement না করা type ঢোকালে compile error — type safety অক্ষুণ্ণ।
  • Library design-এ trait object দিয়ে user-defined extension allow করা সহজ হয়।