Skip to main content

How Rust Decides Which Code to Run

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 which speak 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

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

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

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