Trait Objects
TypeScript interfaces vs Rust trait objects
หัวข้อที่มีชื่อว่า “TypeScript interfaces vs Rust trait objects”TypeScript interfaces เปิดใช้งาน runtime polymorphism โดยไม่ต้องทำอะไรพิเศษ — คุณส่ง object ใดก็ได้ที่ตรงกับ shape ของ interface คู่เทียบของ Rust คือ trait object: ค่าที่นำหน้าด้วย dyn ซึ่งบอก compiler ให้ใช้ vtable สำหรับ dynamic dispatch แทนการ monomorphise
คุณมีสองทางเลือกใน Rust:
| วิธี | Syntax | Dispatch | ใช้เมื่อ |
|---|---|---|---|
| Generic with bound | fn foo<T: Animal>(a: T) | Static (monomorphised) | เมื่อรู้ทุก type ตอน compile time |
| Trait object | fn foo(a: &dyn Animal) | Dynamic (vtable) | เมื่อ concrete type ไม่รู้ตอน compile time |
// TypeScript — interface polymorphisminterface Animal { speak(): string; name(): string;}
class Dog implements Animal { speak() { return "Woof"; } name() { return "Rex"; }}
class Cat implements Animal { speak() { return "Meow"; } name() { return "Whiskers"; }}
function makeNoise(animal: Animal): void { console.log(`${animal.name()} says ${animal.speak()}`);}
const animals: Animal[] = [new Dog(), new Cat()];animals.forEach(makeNoise);trait Animal { fn speak(&self) -> &str; fn name(&self) -> &str;}
struct Dog { name: String }struct Cat { name: String }
impl Animal for Dog { fn speak(&self) -> &str { "Woof" } fn name(&self) -> &str { &self.name }}impl Animal for Cat { fn speak(&self) -> &str { "Meow" } fn name(&self) -> &str { &self.name }}
// &dyn Animal = reference to any type implementing Animalfn make_noise(animal: &dyn Animal) { println!("{} says {}", animal.name(), animal.speak());}
fn main() { // Vec of boxed trait objects — types can differ at runtime let animals: Vec<Box<dyn Animal>> = vec![ Box::new(Dog { name: String::from("Rex") }), Box::new(Cat { name: String::from("Whiskers") }), ]; for a in &animals { make_noise(a.as_ref()); }}Box<dyn Trait> — heap-allocated trait objects
หัวข้อที่มีชื่อว่า “Box<dyn Trait> — heap-allocated trait objects”เมื่อต้องการเก็บ trait object ไว้ใน collection (เช่น Vec) หรือ return ออกจาก function คุณต้อง box ก่อน ตัว Box<dyn Trait> เป็น fat pointer: เก็บทั้ง address ของข้อมูลบน heap และ pointer ไปยัง vtable
// ส่งคืน trait object จากฟังก์ชันfn new_animal(kind: &str) -> Box<dyn Animal> { match kind { "dog" => Box::new(Dog { name: String::from("Rex") }), _ => Box::new(Cat { name: String::from("Whiskers") }), }}นี่คือคู่เทียบของ Rust กับการ return interface type จาก TypeScript factory function
กฎ Object-safety
หัวข้อที่มีชื่อว่า “กฎ Object-safety”ไม่ใช่ทุก trait จะทำเป็น trait object ได้ Trait มีความ object-safe เมื่อ:
- ไม่มี generic methods (generics จะต้อง monomorphise ซึ่งทำลาย vtable)
- Methods ไม่ return
Selfโดยค่า
Clone ใน standard library ไม่ object-safe เพราะ clone() return Self ส่วน Display และ Debug เป็น object-safe
ลองเล่นดู
หัวข้อที่มีชื่อว่า “ลองเล่นดู”trait Shape { fn area(&self) -> f64; fn name(&self) -> &str;}
struct Circle { radius: f64 }struct Rectangle { width: f64, height: f64 }
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn name(&self) -> &str { "Circle" }}impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn name(&self) -> &str { "Rectangle" }}
fn print_area(shape: &dyn Shape) { println!("{}: area = {:.2}", shape.name(), shape.area());}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 3.0 }), Box::new(Rectangle { width: 4.0, height: 5.0 }), Box::new(Circle { radius: 1.5 }), ];
for s in &shapes { print_area(s.as_ref()); }
let total: f64 = shapes.iter().map(|s| s.area()).sum(); println!("Total area: {:.2}", total);}Compiling…