A Didactic dyn Trait Example

https://gist.github.com/rust-play/83b8b2bde4da2e392c42aab4482394d5

dyn Trait does not unify implementations — it only erases the type.

error[E0308]: mismatched types

a Vec<T> must have one concrete type

Try generics:

fn run_all<T: Action>(actions: Vec<T>) {
    for a in actions {
        a.run();
    }
}

This works only if all elements are the same concrete type.

Generics require:

  • Knowing all concrete types at compile time
  • One monomorphized function per type

But here:

  • Types are mixed
  • Stored together
  • Chosen at runtime

This is runtime polymorphism, not compile-time polymorphism.

But our problem is:

We want different concrete types in the same collection.

Generics cannot express:

“Many types, same interface, chosen at runtime”

When you SHOULD use dyn Trait

This example captures the canonical use case:

✅ Heterogeneous collections
✅ Runtime plugin systems
✅ Event handlers / callbacks
✅ Command pattern
✅ Dependency injection boundaries

Enter dyn Trait (this is the moment)

let actions: Vec<Box<dyn Action>> = vec![
    Box::new(PrintAction),
    Box::new(SaveAction),
];

Why this works:

  • Box<dyn Action> has a fixed size
  • Each element is a fat pointer:
    • data pointer
    • vtable pointer
  • The vector doesn’t care what the concrete type is

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6b5d6f5eea496e3596cb9410a67a2ff2

Using dyn Trait is a compile-time design decision that enables runtime polymorphism, not a runtime workaround.

Where the choice is made

The choice happens here:

Vec<Box<dyn Action>>

That type annotation:

  • Forces heap allocation
  • Forces fat pointers
  • Forces vtables
  • Forces dynamic dispatch

main is just where the decision is written, not where it’s executed.

How Rust solves this

Rust says:

“You may use dyn Trait, but only behind something with a known size.”

That “something” is a pointer, e.g.:

&dyn Action
Box<dyn Action>
Arc<dyn Action>

These all have a known, fixed size.