Rust Trait Objects
On the Traits page you wrote functions that accept “any type that implements Speak”. On the Generics page you saw how Rust makes a separate copy of a generic function for each type. Both work great when you know the type at compile time. But what if you want one list that holds many different types?
That’s what a trait object is for. Think of a music teacher with a room full of students. One plays piano, one plays violin, one plays drums. The teacher doesn’t need to know which instrument each student has — they just say “play!” and each student plays their own way. A trait object lets your code do the same: “I don’t know the exact type, but I know it can do this.”
The problem: one Vec, different types
Section titled “The problem: one Vec, different types”Say you have two shapes, and you want to keep them in one Vec:
struct Circle { radius: f64 }struct Square { side: f64 }
fn main() { let shapes = vec![Circle { radius: 1.0 }, Square { side: 2.0 }];}This doesn’t compile:
error[E0308]: mismatched types --> src/main.rs:5:47 |5 | let shapes = vec![Circle { radius: 1.0 }, Square { side: 2.0 }]; | ^^^^^^^^^^^^^^^^^^^^ expected `Circle`, found `Square`A Vec can only hold one type. Every item must be the same size in memory, so Rust can line them up one after another. A Circle and a Square are different types, so they can’t share a plain Vec.
Box<dyn Trait>: a list of different shapes
Section titled “Box<dyn Trait>: a list of different shapes”The fix has two parts. First, give both shapes a shared trait. Second, put each shape in a Box and say the type is Box<dyn Shape>:
trait Shape { fn name(&self) -> String; fn area(&self) -> f64;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn name(&self) -> String { String::from("circle") } fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }}
impl Shape for Square { fn name(&self) -> String { String::from("square") } fn area(&self) -> f64 { self.side * self.side }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 2.0 }), Box::new(Square { side: 3.0 }), Box::new(Circle { radius: 1.0 }), ];
for shape in &shapes { println!("{} has area {:.2}", shape.name(), shape.area()); }}Output:
circle has area 12.57square has area 9.00circle has area 3.14Let’s read Box<dyn Shape> piece by piece:
dyn Shapemeans “some type that implementsShape— we’ll find out which one when the program runs”.dynis short for dynamic.Boxputs the value on the heap and keeps a pointer to it (you metBoxon the Smart Pointers page). EveryBoxis the same size, no matter what is inside it. That’s why theVecis happy now.
The value inside the box (plus a hidden pointer that says “this one is a Circle”) is what we call a trait object.
&dyn Trait: borrowing instead of boxing
Section titled “&dyn Trait: borrowing instead of boxing”You don’t always need a Box. If the values already live somewhere else, a reference works too. &dyn Shape means “a borrowed reference to some type that implements Shape”:
trait Shape { fn area(&self) -> f64;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }}
impl Shape for Square { fn area(&self) -> f64 { self.side * self.side }}
fn print_area(shape: &dyn Shape) { println!("Area: {:.1}", shape.area());}
fn main() { let circle = Circle { radius: 1.0 }; let square = Square { side: 4.0 };
print_area(&circle); print_area(&square);
let both: Vec<&dyn Shape> = vec![&circle, &square]; println!("We have {} shapes", both.len());}Output:
Area: 3.1Area: 16.0We have 2 shapesA simple rule: use Box<dyn Trait> when the list should own its values, and &dyn Trait when you only need to borrow them for a while.
Static vs dynamic dispatch
Section titled “Static vs dynamic dispatch”“Dispatch” just means “deciding which method to call”. Rust can do this in two ways.
Static dispatch happens with generics and impl Trait. The compiler knows the exact type, so it makes a special copy of the function for each type (as you saw on the Generics page). The call goes straight to the right method. It’s very fast.
Dynamic dispatch happens with dyn Trait. The compiler doesn’t know the exact type. So each trait object carries a small hidden table (called a vtable) that says “for this value, area lives here”. At runtime, Rust looks up the method in that table, then calls it. That lookup is a tiny extra cost.
// Static dispatch: Rust makes one copy of this function per typefn static_area(shape: &impl Shape) -> f64 { shape.area()}
// Dynamic dispatch: one function, the method is looked up at runtimefn dynamic_area(shape: &dyn Shape) -> f64 { shape.area()}Both functions give the same answer for the same shape. The only difference is how Rust finds the right area method. So which one should you pick?
Use generics / impl Trait when… |
Use dyn Trait when… |
|---|---|
| Each call works with one known type | You need to mix different types in one collection |
| You want the fastest possible code | You decide the type at runtime (for example, from user input) |
| You don’t mind a slightly bigger program | You want smaller code and faster compile times |
A good default for beginners: start with generics or impl Trait. Reach for dyn when you hit the “different types in one list” problem. Don’t worry too much about the speed difference — in most programs you will never notice it.
Returning Box<dyn Trait> from a function
Section titled “Returning Box<dyn Trait> from a function”Sometimes a function needs to decide at runtime which type to return. You might try -> impl Shape, but that only works when the function always returns the same type. If different branches return different types, return a Box<dyn Shape> instead:
trait Shape { fn describe(&self) -> String;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn describe(&self) -> String { format!("a circle with radius {}", self.radius) }}
impl Shape for Square { fn describe(&self) -> String { format!("a square with side {}", self.side) }}
fn make_shape(kind: &str, size: f64) -> Box<dyn Shape> { if kind == "circle" { Box::new(Circle { radius: size }) } else { Box::new(Square { side: size }) }}
fn main() { let a = make_shape("circle", 1.5); let b = make_shape("square", 2.0); println!("I made {}", a.describe()); println!("I made {}", b.describe());}Output:
I made a circle with radius 1.5I made a square with side 2This is a common pattern for “factory” functions, plugin systems, and config-driven code — anywhere the exact type depends on data you only see when the program runs.
A practical example: sending notifications
Section titled “A practical example: sending notifications”Here is something closer to a real app. Your program needs to notify a user, but each user has chosen different channels: email, SMS, or both. Each channel is its own type, and they all share a Notifier trait:
trait Notifier { fn send(&self, message: &str) -> String;}
struct Email { address: String }struct Sms { phone: String }
impl Notifier for Email { fn send(&self, message: &str) -> String { format!("Email to {}: {}", self.address, message) }}
impl Notifier for Sms { fn send(&self, message: &str) -> String { format!("SMS to {}: {}", self.phone, message) }}
fn notify_all(channels: &[Box<dyn Notifier>], message: &str) { for channel in channels { println!("{}", channel.send(message)); }}
fn main() { let channels: Vec<Box<dyn Notifier>> = vec![ Box::new(Email { address: String::from("ana@example.com") }), Box::new(Sms { phone: String::from("+1-555-0100") }), ];
notify_all(&channels, "Your order has shipped!");}Output:
Email to ana@example.com: Your order has shipped!SMS to +1-555-0100: Your order has shipped!The nice part: if you later add a Push notifier, you write one new struct and one impl Notifier for Push. The notify_all function doesn’t change at all.
The limits: dyn compatibility
Section titled “The limits: dyn compatibility”Not every trait can become a trait object. A trait must be dyn compatible (older books and blog posts call this object safety). The full rules are long, but two cover most real cases:
- A method can’t return
Self. (Behind adyn, Rust doesn’t know whatSelfis, so it can’t know how big the return value is.) - A method can’t have its own generic type parameters, like
fn process<T>(&self, item: T).
Here’s what happens if you break the first rule:
trait Shape { fn area(&self) -> f64; fn duplicate(&self) -> Self;}// ... later:let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 })];error[E0038]: the trait `Shape` is not dyn compatible |14 | let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 })]; | ^^^^^ `Shape` is not dyn compatible | 1 | trait Shape { | ----- this trait is not dyn compatible... 2 | fn area(&self) -> f64; 3 | fn duplicate(&self) -> Self; | ^^^^ ...because method `duplicate` references the `Self` type in its return type = help: consider moving `duplicate` to another traitThe error is honest and helpful: it tells you exactly which method is the problem. Your options are to move that method into a different trait, or add where Self: Sized to the method. That tells Rust “this method only exists on real, known types, not on trait objects”:
trait Shape { fn area(&self) -> f64; fn duplicate(&self) -> Self where Self: Sized;}This is also why some standard traits, like Clone, can’t be used as dyn Clone — clone() returns Self.
Common mistakes
Section titled “Common mistakes”Forgetting the Box. It’s tempting to write Vec<dyn Shape>:
let shapes: Vec<dyn Shape> = Vec::new();error[E0277]: the size for values of type `dyn Shape` cannot be known at compilation time --> src/main.rs:3:17 |3 | let shapes: Vec<dyn Shape> = Vec::new(); | ^^^^^^^^^^^^^^ doesn't have a size known at compile-time | = help: the trait `Sized` is not implemented for `dyn Shape`A dyn Shape could be any size — a small Square or a big struct with many fields. Rust needs a fixed size, so a trait object must always live behind a pointer: Box<dyn Shape>, &dyn Shape, or Rc<dyn Shape>.
Using dyn everywhere. If a function only ever gets one type at a time, impl Trait or generics are simpler and faster (the Advanced Traits page has more on impl Trait). Save dyn for when you really need to mix types.
Practice
Section titled “Practice”1. A zoo in one Vec
Section titled “1. A zoo in one Vec”Define a trait Animal with fn name(&self) -> String and fn sound(&self) -> String. Implement it for Dog, Cow and Duck, put one of each into a Vec<Box<dyn Animal>>, and loop over it to print what each animal says. Hint: the structs can be empty (struct Dog;), and each one goes in with Box::new(...).
Show solution
trait Animal { fn name(&self) -> String; fn sound(&self) -> String;}
struct Dog;struct Cow;struct Duck;
impl Animal for Dog { fn name(&self) -> String { String::from("Dog") } fn sound(&self) -> String { String::from("woof") }}
impl Animal for Cow { fn name(&self) -> String { String::from("Cow") } fn sound(&self) -> String { String::from("moo") }}
impl Animal for Duck { fn name(&self) -> String { String::from("Duck") } fn sound(&self) -> String { String::from("quack") }}
fn main() { let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cow), Box::new(Duck)];
for animal in &zoo { println!("{} says {}", animal.name(), animal.sound()); }}Output:
Dog says woofCow says mooDuck says quack2. Pick a greeter at runtime
Section titled “2. Pick a greeter at runtime”Define a trait Greeter with fn hello(&self, name: &str) -> String, and two types: English and Spanish. Write fn make_greeter(lang: &str) -> Box<dyn Greeter> that returns Spanish for "es" and English for anything else. Greet "Mia" for each code in ["en", "es", "fr"]. Hint: both branches of the if must return the same type, so wrap each one in Box::new(...).
Show solution
trait Greeter { fn hello(&self, name: &str) -> String;}
struct English;struct Spanish;
impl Greeter for English { fn hello(&self, name: &str) -> String { format!("Hello, {name}!") }}
impl Greeter for Spanish { fn hello(&self, name: &str) -> String { format!("¡Hola, {name}!") }}
fn make_greeter(lang: &str) -> Box<dyn Greeter> { if lang == "es" { Box::new(Spanish) } else { Box::new(English) }}
fn main() { for lang in ["en", "es", "fr"] { let greeter = make_greeter(lang); println!("{lang}: {}", greeter.hello("Mia")); }}Output:
en: Hello, Mia!es: ¡Hola, Mia!fr: Hello, Mia!- A
Veccan only hold one type. To mix types, use a trait object likeBox<dyn Trait>. Box<dyn Trait>owns the value;&dyn Traitborrows it. A trait object always lives behind a pointer.- Generics and
impl Traituse static dispatch (fast, decided at compile time).dyn Traituses dynamic dispatch (a vtable lookup at runtime). - Return
Box<dyn Trait>when a function picks between different types at runtime. - Only dyn compatible traits can be trait objects — no methods returning
Selfand no generic methods (unless they havewhere Self: Sized).
Quick check
Section titled “Quick check”Quick check
1. Why can't you put a Circle and a Square in the same plain Vec?
2. What does dyn in Box<dyn Shape> tell Rust?
3. Which of these uses static dispatch?
4. Why is Vec<dyn Shape> (without Box) an error?
5. Which trait method stops a trait from being dyn compatible?
Score: 0 / 5
Read more
Section titled “Read more”- Using Trait Objects to Abstract over Shared Behavior — The Rust Book: the official chapter, building a GUI-style list of components with
Box<dyn Draw>. - Returning Traits with dyn — Rust by Example: a short, runnable example of a function that returns
Box<dyn Trait>. - Trait objects — The Rust Reference: the exact rules for
dyn Traittypes, including lifetimes and extra marker traits likeSend. - Dyn compatibility — The Rust Reference: the full list of rules a trait must follow to be used with
dyn. - The
dynkeyword — std docs: a quick explanation ofdyn, vtables, and the trade-offs compared to generics.