Skip to content

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.”

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.57
square has area 9.00
circle has area 3.14
▶ Try it Yourself

Let’s read Box<dyn Shape> piece by piece:

  • dyn Shape means “some type that implements Shape — we’ll find out which one when the program runs”. dyn is short for dynamic.
  • Box puts the value on the heap and keeps a pointer to it (you met Box on the Smart Pointers page). Every Box is the same size, no matter what is inside it. That’s why the Vec is 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.

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.1
Area: 16.0
We have 2 shapes
▶ Try it Yourself

A 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.

“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 type
fn static_area(shape: &impl Shape) -> f64 {
shape.area()
}
// Dynamic dispatch: one function, the method is looked up at runtime
fn 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.

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.5
I made a square with side 2
▶ Try it Yourself

This 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!
▶ Try it Yourself

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.

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 a dyn, Rust doesn’t know what Self is, 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 trait

The 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.

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.

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 woof
Cow says moo
Duck says quack
▶ Try it Yourself

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!
▶ Try it Yourself
  • A Vec can only hold one type. To mix types, use a trait object like Box<dyn Trait>.
  • Box<dyn Trait> owns the value; &dyn Trait borrows it. A trait object always lives behind a pointer.
  • Generics and impl Trait use static dispatch (fast, decided at compile time). dyn Trait uses 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 Self and no generic methods (unless they have where Self: Sized).

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