Trait Objects vs Generics: When to Use Which
"Trait bound or trait object?" If you've written more than a few Rust functions, you've hit this fork in the road.
Here's my rule of thumb: reach for a trait object when you need flexibility — heterogeneous types, a smaller binary, or a case where you genuinely don't know the concrete type until runtime. Reach for a generic (trait bound) when you're optimizing for performance and want the compiler working for you.
Both come with trade-offs. Let's get into why.
Generics: fast, but bigger
fn process<T: Speak>(item: T) {
item.speak();
}When you write this, Rust uses monomorphization. At compile time, it generates a separate implementation of process for every concrete type you actually call it with. If you call process(dog) and process(cat), you get two versions of that function baked into your binary — one specialized for Dog, one for Cat.
The upside: calling item.speak() is exactly as fast as calling a local function, because the compiler knows the exact type. It has everything it needs at compile time to inline, optimize, simplify — whatever it wants.
The downside: your binary grows. Every concrete type gets its own copy of the code.
Trait objects: flexible, but slower
fn process(item: &dyn Speak) {
item.speak();
}Here, Rust doesn't know the size of item at compile time. That's the whole point — dyn Speak just promises "this type implements Speak." It could be 1 byte, it could be 16. Rust can't make that promise concrete, so you box it (or pass a reference), and you get a fat pointer: one pointer to the value on the heap, one pointer to a vtable — a lookup table with that type's implementation of the trait.
This is dynamic dispatch. At runtime, Rust checks the vtable to find the right method for that specific type. That lookup is the overhead you're trading for flexibility.
The upside: one function, one copy in the binary, works with any type that implements the trait — even types you don't know about yet, mixed together in the same Vec.
The trade-off, side by side
| Generics (trait bound) | Trait objects (dyn) | |
|---|---|---|
| Dispatch | Static (compile time) | Dynamic (runtime, via vtable) |
| Speed | As fast as a local call | Small overhead per call |
| Binary size | Bigger (one copy per type) | Smaller (one shared copy) |
| Flexibility | Fixed at compile time | Can mix types at runtime |
So which one?
Most of the time, you'll use generics. They're everywhere in Rust for a reason — better performance, more compiler optimizations, and honestly, most code doesn't need runtime polymorphism.
Trait objects are for the specific cases: you're building something like a plugin system, a Vec<Box<dyn Widget>> where widgets are genuinely different types, or you're in an embedded context where binary size actually matters more than a few nanoseconds per call.
It's not "which one is better" — it's "what is this specific piece of code optimizing for."