Why use Dynamic Dispatch in Rust?


dynamic dispatch

Dynamic dispatch is a powerful feature in Rust that enables flexibility when working with different types that share a common trait. If you’re just past the beginner stage in Rust, this guide will help you understand dynamic dispatch and its practical applications through an engaging challenge.


Understanding the Challenge: An Inventory System

To demonstrate dynamic dispatch, we’ll build a simple inventory system for a game. The inventory should hold different types of items, such as potions and bombs, each with unique behaviour when used. Here’s what we need to achieve:

  1. Define a shared Item trait that requires a use_item(&self) -> String method.
  2. Implement the Item trait for different types:
    • A Potion that heals the player.
    • A Bomb that damages enemies.
  3. Use dynamic dispatch to store these items in a single collection (Inventory).
  4. Iterate over the items and call their use_item methods to demonstrate their behavior.

Problem with Heterogeneous Types

In Rust, vectors (Vec) are homogeneous, meaning all elements must have the same type. This poses a problem when you want to store different types, like Potion and Bomb, in the same collection. Here’s an example of what won’t work:

let items = vec![Potion, Bomb]; // Error: Different types cannot coexist in a Vec.

To solve this, we’ll use trait objects with dynamic dispatch.


What is Dynamic Dispatch?

Dynamic dispatch is Rust’s way of allowing method calls on objects of different types through a shared interface (a trait). It works by deferring the decision of which method implementation to call until runtime. This enables flexibility, but requires using a layer of indirection.

Enter Box<dyn Trait>:

  • A trait object (dyn Trait) stores the type-erased information of any object implementing the trait.
  • Box ensures these objects are heap-allocated, so they all share a fixed size (a pointer).

By storing Box<dyn Item> in our inventory, we can include both Potion and Bomb in the same collection.


The Solution Code

Here’s the complete implementation:

trait Item {
    fn use_item(&self) -> String;
}

struct Potion;
impl Item for Potion {
    fn use_item(&self) -> String {
        "You used a Potion. You feel rejuvenated!".to_string()
    }
}

struct Bomb;
impl Item for Bomb {
    fn use_item(&self) -> String {
        "You used a Bomb. Boom! It dealt 50 damage to enemies!".to_string()
    }
}

struct Inventory {
    items: Vec<Box<dyn Item>>,
}

impl Inventory {
    fn use_all_items(&self) {
        for item in &self.items {
            println!("{}", item.use_item());
        }
    }
}

fn main() {
    let item1 = Potion;
    let item2 = Bomb;
    let inv = Inventory {
        items: vec![Box::new(item1), Box::new(item2)],
    };

    inv.use_all_items();
}

Key Features of the Solution

  1. Trait Definition: The Item trait defines the shared interface with a use_item method.
  2. Struct Implementations: Both Potion and Bomb implement the Item trait, providing unique behavior for the use_item method.
  3. Dynamic Dispatch: Using Box<dyn Item>, the inventory stores heterogeneous types (Potion and Bomb) while providing a uniform interface to interact with them.
  4. Iteration and Usage: The use_all_items method iterates over the items and dynamically dispatches the use_item method for each one.

Dynamic dispatch is an essential tool for Rust developers, particularly when you need flexibility with collections of heterogeneous types

“Without Box, you couldn’t store types of different sizes in the same vector.”

Example Output

Running the program produces:

You used a Potion. You feel rejuvenated!
You used a Bomb. Boom! It dealt 50 damage to enemies!

Visualizing Dynamic Dispatch

Think of Box<dyn Item> as a universal adapter:

  • The dyn Item trait defines the “plug shape” that all item types must conform to.
  • The Box ensures all items fit uniformly into the “power strip” (the vector).

Without Box, you couldn’t store types of different sizes in the same vector.


Why Use Dynamic Dispatch?

Dynamic dispatch offers several benefits:

  • Flexibility: Store and handle multiple types in a uniform way.
  • Encapsulation: Hide the concrete types behind a shared trait interface.
  • Runtime Polymorphism: Let the program decide which method to call at runtime.

Recap and Takeaways

Dynamic dispatch is an essential tool for Rust developers, particularly when you need flexibility with collections of heterogeneous types. This challenge introduced:

  1. How to define and implement traits.
  2. The role of Box<dyn Trait> in enabling dynamic dispatch.
  3. Practical usage through a game inventory system.

Dynamic dispatch adds a layer of runtime flexibility to Rust’s otherwise strict type system, making it indispensable for many scenarios.

Dynamic Dispatch in Rust


Ready for More Challenges?

If you found this exercise useful, try experimenting with:

  • Adding new item types (e.g., Shield, Sword) and extending their behavior.
  • Implementing inventory modifications, such as removing items after use.
  • Exploring static dispatch alternatives and comparing their performance.