Nugget 8
Home
You want a Vec that holds both Dogs and Cats. With generics,
every element must be the same type. Trait objects change that.
A Vec<Dog> can only hold Dogs. A Vec<Cat>
can only hold Cats. What if you need both?
trait Speak {
fn make_sound(&self) -> String;
}
struct Dog { name: String }
struct Cat { name: String }
impl Speak for Dog {
fn make_sound(&self) -> String {
format!("{} says Woof!", self.name)
}
}
impl Speak for Cat {
fn make_sound(&self) -> String {
format!("{} says Meow!", self.name)
}
}
// This won't compile:
let animals: Vec<???> = vec![
Dog { name: "Rex".into() },
Cat { name: "Whiskers".into() }, // ❌ expected Dog, found Cat
];
Generic collections (Vec<T>) require a single concrete type.
But you want heterogeneous items that share a trait.
Box<dyn Speak>
Wrap each value in Box and type the collection as
Vec<Box<dyn Speak>>:
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog { name: "Rex".into() }),
Box::new(Cat { name: "Whiskers".into() }),
];
// All animals share the Speak interface
for animal in &animals {
println!("{}", animal.make_sound());
}
// Rex says Woof!
// Whiskers says Meow!
This is dynamic dispatch — at runtime, each
make_sound() call looks up which type's implementation to run.
A trait object (dyn Speak) stores two pointers: one to the data,
and one to a vtable — a table of function pointers for each
trait method:
// What Box<dyn Speak> stores internally:
//
// ┌──────────────┐ ┌─────────────────────┐
// │ data pointer │───→│ Dog { "Rex" } │
// ├──────────────┤ └─────────────────────┘
// │ vtable ptr │───→┌─────────────────────┐
// └──────────────┘ │ make_sound → Dog::… │
// │ drop → Dog::drop │
// └─────────────────────┘
// Every make_sound() call goes through the vtable:
// That's one pointer indirection at runtime.
📐 Static vs dynamic dispatch
Generic functions (Nugget 6) use static dispatch — the compiler creates a separate function for each type. Trait objects use dynamic dispatch — one function, looked up at runtime. Static dispatch is faster; dynamic dispatch is more flexible.
Not every trait can be made into a trait object. The compiler enforces object safety rules:
// ✅ Object-safe — no Self return type
trait Speak { fn make_sound(&self) -> String; }
// ❌ Not object-safe — returns Self
trait Clone { fn clone(&self) -> Self; }
// ❌ Not object-safe — generic method
trait IntoIterator {
fn into_iter(self) -> IntoIter<Self::Item>;
}
// The error:
// let v: Vec<Box<dyn Clone>> = vec![];
// error: the trait `Clone` cannot be made into an object
💡 Rule of thumb
If a trait's methods take self by value or return Self,
or have generic type parameters, it can't be a trait object. Most
"interface-like" traits (that take &self) are safe.
Trait objects are everywhere in Rust. Here are two common patterns:
// GUI components
let widgets: Vec<Box<dyn Widget>> = vec![
Box::new(Button::new("Click me")),
Box::new(TextInput::new("Type here")),
Box::new(Checkbox::new(true)),
];
for widget in &widgets {
widget.render(); // each one renders differently
}
// Error handling (Box<dyn Error>)
use std::error::Error;
fn do_stuff() -> Result<(), Box<dyn Error>> {
let _ = std::fs::read_to_string("file.txt")?; // io::Error
let _ = "hello".parse::<i32>()?; // ParseIntError
Ok(())
}
// Box<dyn Error> wraps any error type — the most flexible return type
| Generics (static) | Trait Objects (dynamic) | |
|---|---|---|
| Syntax | <T: Speak> |
Box<dyn Speak> |
| Dispatch | At compile time | At runtime (vtable lookup) |
| Performance | Zero-cost (inlined) | Small indirection cost |
| Collection types | Homogeneous only | Heterogeneous |
| Return types | Concrete type per call | Single type erased |
| When to use | Most of the time | When types vary at runtime |
Vec<Box<dyn Trait>> stores different types that share a trait.Self or with generics block it.Box<dyn Error> is the idiomatic way to return multiple error types from a function.