ข้ามไปยังเนื้อหา

Trait Objects

TypeScript interfaces เปิดใช้งาน runtime polymorphism โดยไม่ต้องทำอะไรพิเศษ — คุณส่ง object ใดก็ได้ที่ตรงกับ shape ของ interface คู่เทียบของ Rust คือ trait object: ค่าที่นำหน้าด้วย dyn ซึ่งบอก compiler ให้ใช้ vtable สำหรับ dynamic dispatch แทนการ monomorphise

คุณมีสองทางเลือกใน Rust:

วิธีSyntaxDispatchใช้เมื่อ
Generic with boundfn foo<T: Animal>(a: T)Static (monomorphised)เมื่อรู้ทุก type ตอน compile time
Trait objectfn foo(a: &dyn Animal)Dynamic (vtable)เมื่อ concrete type ไม่รู้ตอน compile time
TypeScript
// TypeScript — interface polymorphism
interface 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);
Rust
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 Animal
fn 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());
}
}

เมื่อต้องการเก็บ 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

ไม่ใช่ทุก trait จะทำเป็น trait object ได้ Trait มีความ object-safe เมื่อ:

  1. ไม่มี generic methods (generics จะต้อง monomorphise ซึ่งทำลาย vtable)
  2. 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);
}
Trait object ใน Rust คืออะไร?
ทำไม trait objects มักต้องเก็บเป็น `Box<dyn Trait>` แทนที่จะเป็น `dyn Trait` ตรงๆ?
ควรใช้ `&dyn Trait` แทน `<T: Trait>` เมื่อใด?
Trait ใดใน standard library ที่ไม่ object-safe?