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.