Trait Objects & dyn

Reviewed & published by Brayan K

A trait object — written dyn Trait behind a pointer like &dyn Trait or Box<dyn Trait> — lets values of different concrete types be used through one shared interface, with the right method chosen at runtime.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll learn how trait objects differ from generics, how to store mixed types in a Vec<Box<dyn Trait>>, the difference between static and dynamic dispatch, and the object-safety rules that decide when a trait can become a trait object.

What You'll Learn in This Lesson

1️⃣ The dyn Keyword: One Interface, Many Types

When you write &dyn Animal, you're saying "a reference to some type that implements Animal — I don't care which." This is a trait object. Unlike a concrete type, its exact identity is only known at runtime, so the call to a.name() is dispatched dynamically. The dyn keyword makes that runtime nature explicit in the type.

// Trait objects let different concrete types share one interface at runtime.
trait Animal {
    fn name(&self) -> String;
    fn sound(&self) -> String;
}

struct Dog;
struct Cat;
struct Cow;

impl Animal for Dog {
    fn name(&self) -> String { String::from("Dog") }
    fn sound(&self) -> String { String::from("Woof") }
}
impl Animal for Cat {
    fn name(&self) -> String { String::from("Cat") }
    fn sound(&self) -> String { String::from("Meow") }
}
impl Animal for Cow {
    fn name(&self) -> String { String::from("Cow") }
    fn sound(&self) -> String { String::from("Moo") }
}

// `&dyn Animal` is a trait object reference — it can point at ANY Animal.
fn describe(a: &dyn Animal) {
    println!("The {} says {}", a.name(), a.sound());
}

fn main() {
    describe(&Dog);
    describe(&Cat);
    describe(&Cow);
}

A single describe function handles every animal. There is exactly one copy of describe in the binary, and at each call the program follows the trait object's vtable to find the correct name and sound implementation.

2️⃣ Box<dyn Trait>: Mixing Types in a Vec

A Vec requires all its elements to be the same type, so you can't put a Circle and a Square directly in one Vec. The fix is to box each value as a trait object: Vec<Box<dyn Shape>>. Each Box<dyn Shape> owns one heap value and has a uniform pointer size, so they all fit in one vector.

// A Vec can only hold ONE type. To mix types behind a shared trait, store
// trait objects on the heap: Vec<Box<dyn Trait>>.
trait Shape {
    fn area(&self) -> f64;
    fn label(&self) -> &str;
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }
    fn label(&self) -> &str { "Circle" }
}
impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.side }
    fn label(&self) -> &str { "Square" }
}

fn main() {
    // Each Box<dyn Shape> owns one heap value; the Vec holds the boxes.
    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 }),
    ];

    let mut total = 0.0;
    for shape in &shapes {
        // Dynamic dispatch: the right area() is chosen at runtime per element.
        println!("{} area = {:.2}", shape.label(), shape.area());
        total += shape.area();
    }
    println!("Total area = {:.2}", total);
}

Use &dyn Trait when you only need to borrow for a call, and Box<dyn Trait> when you need to own the value — in a collection, as a struct field, or as a return type that might be one of several types.

3️⃣ Static vs Dynamic Dispatch

Generics give you static dispatch: the compiler stamps out a specialized copy of the function for each concrete type (monomorphization), so calls are resolved at compile time and can be inlined. Trait objects give you dynamic dispatch: one shared function, with each call routed through a vtable at runtime. Compare the two side by side below.

// Static dispatch (generics) vs dynamic dispatch (trait objects).
trait Greet {
    fn hello(&self) -> String;
}

struct English;
struct Spanish;

impl Greet for English {
    fn hello(&self) -> String { String::from("Hello!") }
}
impl Greet for Spanish {
    fn hello(&self) -> String { String::from("Hola!") }
}

// STATIC dispatch: a separate copy is compiled for each concrete T.
// Fast (often inlined), resolved at compile time. T is one fixed type per call.
fn greet_static<T: Greet>(g: &T) {
    println!("[static]  {}", g.hello());
}

// DYNAMIC dispatch: one function, the call is looked up at runtime via a vtable.
// More flexible (can hold mixed types), tiny runtime cost per call.
fn greet_dynamic(g: &dyn Greet) {
    println!("[dynamic] {}", g.hello());
}

fn main() {
    greet_static(&English);
    greet_static(&Spanish);

    greet_dynamic(&English);
    greet_dynamic(&Spanish);
}

Both print the same thing, but the machine code differs: greet_static is compiled twice (one version per type), while greet_dynamic exists once and looks up the method at runtime. Prefer generics for speed and single-type call sites; prefer trait objects when you need to store or pass mixed types.

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — fill in the trait-object types so this compiles and runs.
trait Drawable {
    fn draw(&self) -> String;
}

struct Button;
struct Checkbox;

impl Drawable for Button {
    fn draw(&self) -> String { String::from("[ Button ]") }
}
impl Drawable for Checkbox {
    fn draw(&self) -> String { String::from("[x] Checkbox") }
}

fn main() {
    // 1) A heterogeneous list needs boxed trait objects.
    let widgets: Vec<Box<dyn ___>> = vec![      // 👉 the trait name
        Box::new(Button),
        Box::new(Checkbox),
    ];

    for w in &widgets {
        render(w.as_ref());
    }
}

// 2) Accept ANY Drawable by reference using a trait object.
fn render(d: &___ Drawable) {                   // 👉 the trait-object keyword
    println!("{}", d.draw());
}

// ✅ Expected output:
//    [ Button ]
//    [x] Checkbox

Mini-Challenge: A Pluggable Notifier

Build a small notification system where many channels share one Notifier trait and live together in a Vec<Box<dyn Notifier>>. Run it with cargo run and check the output.

// 🎯 MINI-CHALLENGE: a pluggable notification system.
//
// 1. Define a trait Notifier with: fn send(&self, msg: &str) -> String.
// 2. Make two structs, Email and Sms, that implement it. Email returns
//    "Email: <msg>" and Sms returns "SMS: <msg>" (use format!).
// 3. In main, build a Vec<Box<dyn Notifier>> holding one of each, then loop
//    and print c.send("Build passed") for every channel.
//
// ✅ Example output:
//    Email: Build passed
//    SMS: Build passed

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Trait Objects

FormMeaning
&dyn Traitborrow any implementor
Box<dyn Trait>own one on the heap
Vec<Box<dyn Trait>>a list of mixed types
<T: Trait>static dispatch (one type)
-> Box<dyn Trait>return one of several types

🎉 Lesson Complete!

Practice quiz

What does dyn Trait represent in Rust?

  • A compile-time generic
  • A macro
  • A trait object resolved at runtime
  • A lifetime

Answer: A trait object resolved at runtime. dyn Trait is a trait object: the concrete type is known only at runtime via a vtable.

Why can't you write a bare dyn Trait by value, like fn f(x: dyn Trait)?

  • Its size is not known at compile time
  • dyn is reserved
  • Traits cannot be parameters
  • It needs a lifetime

Answer: Its size is not known at compile time. A bare trait object is unsized, so it must sit behind a pointer like &dyn Trait or Box<dyn Trait>.

Which type lets you OWN a trait object on the heap?

  • &dyn Trait
  • impl Trait
  • dyn Trait
  • Box<dyn Trait>

Answer: Box<dyn Trait>. Box<dyn Trait> owns one heap-allocated value; the box itself has a known pointer size.

How can you store mixed concrete types that share one trait in a Vec?

  • Vec<dyn Trait>
  • Vec<Box<dyn Trait>>
  • Vec<impl Trait>
  • Vec<Trait>

Answer: Vec<Box<dyn Trait>>. Each element must have the same size, so box each as Vec<Box<dyn Trait>>.

Trait objects use which kind of dispatch?

  • Dynamic dispatch via a vtable
  • Static dispatch via monomorphization
  • No dispatch
  • Inline dispatch only

Answer: Dynamic dispatch via a vtable. Each method call is looked up at runtime through the trait object's vtable.

Generics like fn f<T: Trait>(x: T) use which kind of dispatch?

  • Dynamic dispatch
  • Runtime vtable lookup
  • Static dispatch
  • Reflection

Answer: Static dispatch. The compiler stamps out a specialized copy per concrete type, resolved at compile time.

A trait object reference is described as a fat pointer because it holds...

  • Two data pointers
  • A data pointer plus a vtable pointer
  • A length and a capacity
  • A pointer and a lifetime

Answer: A data pointer plus a vtable pointer. It is two pointers wide: one to the data and one to the vtable of method pointers.

Which compiler error means a trait is not object safe?

  • E0277: trait not implemented
  • E0502: borrow conflict
  • E0412: cannot find type
  • E0038: cannot be made into an object

Answer: E0038: cannot be made into an object. E0038 fires when a trait breaks object-safety rules, such as returning Self.

Which makes a trait NOT object safe?

  • A method taking &self
  • A method that returns Self
  • A method returning String
  • A default method

Answer: A method that returns Self. Returning Self needs a known size, which a trait object lacks, so it breaks object safety.

When should you prefer a generic over dyn Trait?

  • When you need mixed types in a Vec
  • When the type is unknown until runtime
  • When all values are one known type
  • When returning one of several types

Answer: When all values are one known type. If the type is fixed, generics give static dispatch with no runtime cost.

Continue this course

Frequently asked questions

What is the difference between dyn Trait and a generic <T: Trait>?

A generic parameter like fn f<T: Trait>(x: T) uses static dispatch: the compiler generates a specialized copy of the function for each concrete type you actually call it with, and every method call is resolved at compile time (and often inlined). A trait object like &dyn Trait or Box<dyn Trait> uses dynamic dispatch: there is one copy of the code, and each method call is looked up at runtime through a vtable. Generics are faster but every value must be the same single type; trait objects are a little slower but let you mix different concrete types behind one interface — for example in a Vec<Box<dyn Trait>>.

When should I use Box<dyn Trait> versus &dyn Trait?

Use &dyn Trait when you only need to borrow a value for the duration of a call — it is just a fat pointer and owns nothing. Use Box<dyn Trait> when you need to own the value and store it somewhere with an unknown size at compile time, such as in a Vec<Box<dyn Trait>>, as a struct field, or as a return type that could be one of several types. Box puts the value on the heap so the container only needs to hold a pointer of known size.

What does 'object safe' mean and why does my trait fail to become a trait object?

Only object-safe traits can be made into trait objects. A trait is object safe when its methods can be called through a vtable: methods must take &self (not consume self by value in a way that needs a known size), must not return Self, and must not have generic type parameters of their own. Methods with where Self: Sized are allowed but simply are not callable on the trait object. If you see error E0038 ('the trait cannot be made into an object'), one of these rules was broken — commonly a method returning Self or a generic method.

Why is there a small runtime cost to dynamic dispatch?

A trait object is a 'fat pointer': it stores a pointer to the data plus a pointer to a vtable, a small table of function pointers for that concrete type's methods. Calling a method means following the vtable pointer to find the right function, which adds one level of indirection and usually prevents inlining. The cost is tiny — typically a single pointer load — and is irrelevant for the vast majority of code. Reach for generics when a hot loop measures slower with dynamic dispatch; otherwise prefer whichever reads more clearly.