Skip to main content

Using Trait Objects for Dynamic Polymorphism in Rust

Created: October 29, 2025 4 min read

One of the core principles of object-oriented programming is polymorphism: the ability for code to work with values of different types through a common interface. Rust supports polymorphism in two ways: through static dispatch (using generics) and dynamic dispatch (using trait objects).

While generics are often preferred for their performance, trait objects provide a powerful and flexible way to handle situations where the specific type of a value isn’t known at compile time.

What is a Trait Object?

A trait object is a special kind of pointer that allows you to treat different concrete types that implement the same trait as a single type. It consists of two parts:

  1. A pointer to the actual data (the instance of the struct).
  2. A pointer to a virtual method table (vtable), which contains the addresses of the concrete methods for that type.

Because the compiler doesn’t know the size of the concrete type at compile time, a trait object must always be behind a pointer, such as &, Box, or Rc. The syntax for a trait object is dyn Trait, for example &dyn MyTrait or Box<dyn MyTrait>.

When you call a method on a trait object, Rust uses the vtable to look up the correct method address at runtime. This process is called dynamic dispatch.

Use Case: A Heterogeneous Collection

The most common use case for trait objects is to create a collection of different types that all share a common behavior.

Let’s imagine a simple GUI application where we want to render various UI elements. Each element can be drawn, but they are all different types.

// 1. Define the common behavior with a trait.
pub trait Drawable {
    fn draw(&self);
}

// 2. Create different types that implement the trait.
pub struct Button {
    pub label: String,
}

impl Drawable for Button {
    fn draw(&self) {
        println!("Drawing a button with label: '{}'", self.label);
    }
}

pub struct Image {
    pub src: String,
}

impl Drawable for Image {
    fn draw(&self) {
        println!("Drawing an image from source: '{}'", self.src);
    }
}

// 3. Use a vector of trait objects to hold different types.
fn main() {
    let ui_elements: Vec<Box<dyn Drawable>> = vec![
        Box::new(Button {
            label: String::from("Submit"),
        }),
        Box::new(Image {
            src: String::from("/icon.png"),
        }),
    ];

    // 4. Call the method on each element, dispatching dynamically.
    for element in ui_elements {
        element.draw();
    }
}
```
In this example, the `ui_elements` vector can hold both `Button`s and `Image`s because they are stored as `Box<dyn Drawable>` trait objects. The `for` loop doesn't need to know the concrete type of each `element`; it just calls the `draw` method, and Rust figures out the right one to run at runtime.

### Static vs. Dynamic Dispatch

It's important to understand the trade-offs between using trait objects (dynamic dispatch) and generics (static dispatch).

**Static Dispatch with Generics:**

- **How it works:** `fn process<T: Drawable>(item: T) { ... }`
- The compiler generates a specialized version of the function for each concrete type used (monomorphization).
- **Pros:** Fast. Method calls are resolved at compile time and can be inlined.
- **Cons:** Can lead to larger binary sizes. Cannot be used for heterogeneous collections like the example above.

**Dynamic Dispatch with Trait Objects:**

- **How it works:** `fn process(item: &dyn Drawable) { ... }`
- The method call is looked up at runtime using a vtable.
- **Pros:** Allows for heterogeneous collections. Reduces binary size as code is not duplicated.
- **Cons:** Incurs a small runtime performance cost due to pointer indirection and vtable lookup. Prevents some compiler optimizations like inlining.

### Object Safety

Not all traits can be made into trait objects. A trait must be **object-safe**. A trait is object-safe if both of the following are true:

1. The trait does not require `Self: Sized`.
2. All of its methods are object-safe.

A method is object-safe if:

1. It does not have any generic type parameters.
2. Its return type is not `Self`.
3. The first parameter is not `self` by value (`self: Self`).

For example, the `Clone` trait is not object-safe because its `clone` method returns `Self`. If you had a `&dyn Clone`, the compiler wouldn't know the size of the concrete type to allocate for the returned value.

````rust
// This is not object-safe because clone() returns Self.
pub trait Clone {
    fn clone(&self) -> Self;
}

// This would not compile:
// let clonable: &dyn Clone = &5; 
```

### Conclusion

Trait objects are a key feature for enabling OOP-like patterns in Rust. They provide the flexibility to work with different types through a common interface when the concrete types aren't known at compile time. While there is a minor performance cost compared to static dispatch with generics, they are an indispensable tool for building flexible and extensible applications.

## Resources

- [Official Documentation](https://docs.python.org/3/)
- [Language Specification](https://docs.python.org/3/reference/)
- [Community Resources](https://www.python.org/community/)

Comments

Share this article

Scan to read on mobile