Ever wondered how Rust knows which function to run when you call a method? Or why sometimes we write dyn Trait
and other times we use <T: Trait>
?
Rust gives you zero-cost abstractions, but when it comes to polymorphism, choosing between dyn Trait
and generics can be confusing.
This guide will take you from 0 to mastery on Rust dispatching.
This is all about dispatch—how Rust "dispatches" or routes your method calls.
🧠 What Is Dispatch?
Imagine you're calling customer support.
- With a static dispatch, you already know you're talking to a human from billing. Fast, no confusion.
- With a dynamic dispatch, you first talk to a receptionist, who routes you based on your problem. More flexible, but a bit slower.
Rust does the same thing when calling methods.
Dispatch decides which method implementation to call.
- Static Dispatch: Decided at compile time.
- Dynamic Dispatch: Decided at runtime using a vtable.
🛠️ Static (Default)
Rust prefers to know everything at compile time. That way, it can prepare ahead of time and run super fast.
Example:
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
fn make_speak<T: Animal>(animal: T) {
animal.speak(); // Rust knows this is a Dog
}
🧠 What Happens:
- Rust creates a custom version of make_speak for every type you pass.
- This is called monomorphization.
- It’s like having one phone number per department. No middleman. No runtime cost.
✅ Fast
❌ Can’t mix different types easily
🌀 Dynamic
Dynamic Dispatch with dyn Trait
Sometimes, you don’t know the type ahead of time. You just want anything that behaves like an Animal
.
That’s where dyn Trait
comes in—when you need flexibility.
Example:
trait Animal {
fn speak(&self);
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn speak(&self) { println!("Woof!"); }
}
impl Animal for Cat {
fn speak(&self) { println!("Meow!"); }
}
fn make_speak(animal: &dyn Animal) {
animal.speak();
}
Now you can call it with a Dog
, Cat
, or anything else that implements Animal
.
fn main() {
let animals: Vec<&dyn Animal> = vec![&Dog, &Cat];
for a in animals {
make_speak(a); // different animals, same call
}
}
🔍 Under the Hood:
&dyn Animal
is a trait object- It’s a fat pointer =
(data_ptr, vtable_ptr)
a vtable (a kind of method menu) to decide whichspeak
to call at runtime. - Vtable enables runtime dispatch
✅ Great for lists of different types
❌ Slightly slower, can’t use some trait features
🚫 When Not to
Avoid dyn Trait
if:
- You need associated types
- You care about performance
- You're in hot paths like networking, execution (Ethereum clients)
📦 Trait Object/Param
- Trait Object
- Trait Parameter
fn log(value: &dyn std::fmt::Debug) {
println!("{:?}", value);
}
- Runtime polymorphism
- Type info is erased at runtime
- Slightly slower (uses a lookup table)
- Good for mixed-type use cases
fn log<T: std::fmt::Debug>(value: T) {
println!("{:?}", value);
}
- Compile-time polymorphism
- Fast, inlined
- No type erasure
A &dyn Trait
is like saying:
"Give me anything that can do this thing. I don’t care what type it is."
It hides the original type. Rust no longer knows if it’s a Dog or a Cat—only that it can speak()
.
That’s called type erasure.
🧱 Type Erasure?
When you use dyn Trait
, you lose the concrete type. You can only use the trait methods. No introspection. No associated types.
fn handle(val: Box<dyn std::fmt::Debug>) {
println!("{:?}", val); // we don't know if it's i32 or String
}
🧩 Why Static?
Because Rust is about performance and safety. Use static dispatch when:
- You know the type
- You want speed
- You need full trait features (like associated types)
Use dynamic dispatch when:
- You want flexibility
- You're working with plugins, handlers, or mixed lists
- Performance isn’t your biggest concern
🧪 Game time
Can you store both Circle and Square in the same list?
trait Shape { fn draw(&self); }
struct Circle;
struct Square;
impl Shape for Circle { fn draw(&self) { println!("Circle"); } }
impl Shape for Square { fn draw(&self) { println!("Square"); } }
// Try putting them in a Vec<...>?
✅ Answer:
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle),
Box::new(Square),
];
🔥 Real Projects
In Ethereum clients like lighthouse, we avoid dyn in places like:
- P2P networking
- Consensus Because speed matters.
But dyn Trait
is great for:
- Logging
- Plugin systems
- Exposing user-defined behavior
🧠 Final Thought
Use
dyn Trait
when you need to be flexible.
Use
<T: Trait>
when you need to be fast.
🧠 Reference Table
Situation | Use Static (T: Trait ) | Use Dynamic (dyn Trait ) |
---|---|---|
Performance is critical (reth)? | ✅ Yes | ❌ No |
Types known at compile time? | ✅ Yes | ❌ No |
Different types in one list? | ❌ No | ✅ Yes |
You want to expose an extensible API? | ❌ No | ✅ Yes |
Need associated types? | ✅ Yes | ❌ Not supported |
💡 Pro Tip:
If you can write your code without dyn
, do it.
If you can’t, now you know what dyn Trait
really means—and when to reach for it.