Start learning free

LESSON 406 OF 414 · OBJECT-ORIENTED PROGRAMMING

Polymorphism in Rust: Trait Objects and dyn

Lesson Read the full explanation

TL ; DR:

Polymorphism is when a single piece of code (like animal.speak()) does different things depending on which type the value actually is. You write the call once, and the right implementation runs automatically.


let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for a in &animals {
    a.speak();   // dispatches to Dog::speak then Cat::speak
}


Full lesson:

Polymorphism (Greek for "many forms") is what makes traits actually useful at scale. Once Dog and Cat both implement the Animal trait and provide speak(), you can stop caring which one you're holding. Any code that takes a dyn Animal and calls speak() works for both.

Why it matters

Imagine you're writing a function that prints what every animal in a zoo says:

fn roll(animals: &Vec<Box<dyn Animal>>) {
    for a in animals { a.speak(); }
}

This function works for any current or future kind of animal. Add a Cow struct tomorrow — this loop runs Cow::speak() automatically, no edits needed. That's the win: code that works today and keeps working as you add more types.

Static type vs dynamic type

A Box<dyn Animal> is typed as Animal. The value it actually holds is a Dog, or a Cat, etc. Polymorphism is the language dispatching the method call based on the dynamic type (what it is at runtime) rather than the static type (what the box was declared to hold).

Notes by language

  • Trait objects — dynamic dispatch happens only through a trait object such as Box<dyn Animal> or &dyn Animal. No keyword like virtual is needed.
  • Generics — a generic function such as fn f<T: Animal>(a: &T) is resolved at compile time instead (static dispatch).

Your task

Animal, Dog, and Cat are already defined, each with its own speak. Inside main_function, build a Vec<Box<dyn Animal>> containing one Dog and one Cat, then loop through it calling speak() on each.

Expected output:

Woof
Meow
STEP 1 OF 2
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING

    Polymorphism in Rust, explained

    What polymorphism is

    Polymorphism means "many forms": one piece of code works with values of different types, and each value brings its own behaviour. You write animal.speak() once, and a dog prints Woof while a cat prints Meow.

    The payoff is that the calling code does not change when new types appear. A loop that calls speak() on every animal keeps working when someone adds a Cow, because the loop depends only on the contract, the Animal trait, and not on the list of types that exist.

    Rust offers polymorphism in two flavours, and this lesson uses the second:

    • Static dispatch with generics. A function such as fn greet<T: Animal>(a: &T) is compiled separately for each concrete type that calls it. The method is chosen at compile time and the call has no extra cost, but each call site works with exactly one type.
    • Dynamic dispatch with trait objects. A value of type Box<dyn Animal> can hold any type that implements Animal, and the method is looked up at run time. This is what lets one collection hold different types.

    Why a plain Vec does not work

    A Vec stores elements of one type, laid out one after another, so every element must have the same size. Dog and Cat are different types, so this does not compile:

    let animals = vec![Dog, Cat];
    

    The compiler reports error[E0308]: mismatched types, with expected `Dog`, found `Cat`. The fix is to store a uniform kind of element: a Box, a pointer to a value on the heap. A Box<dyn Animal> has the same size for every animal, because it holds a pointer to the value plus a pointer to a table of that type's Animal methods. The call animal.speak() follows that table to the right function.

    Step by step

    1. Declare the vector in main_function with the type Vec<Box<dyn Animal>>. The type annotation is needed so that Rust treats each element as a trait object instead of inferring a concrete type from the first one.
    2. Fill it with vec! from Box::new(Dog) and Box::new(Cat). Each box is converted to Box<dyn Animal> automatically because the annotation says that is what the vector holds. Dog and Cat are unit structs, so their values are written as just the name.
    3. Loop over the vector with for animal in animals and call animal.speak(). Each box is visited in order, and the call runs Dog::speak for the first and Cat::speak for the second.

    The solution in Rust

    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 main_function() {
        let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
        for animal in animals {
            animal.speak();
        }
    }
    

    The output is:

    Woof
    Meow
    

    Static type and dynamic type

    The static type of animal in the loop is Box<dyn Animal>: it is all the compiler knows. The dynamic type is the real one, Dog or Cat. A call through dyn Animal is dispatched on the dynamic type. That is the same idea as a virtual call in other languages, but in Rust it is opt-in: you only pay for it where you write dyn.

    Writing code against the trait

    A function can borrow the whole collection and stay ignorant of the concrete types:

    fn roll(animals: &[Box<dyn Animal>]) {
        for a in animals {
            a.speak();
        }
    }
    

    Adding struct Cow; with its own impl Animal for Cow and pushing Box::new(Cow) onto the vector makes roll print Moo as well, with no edit to roll. The parameter type &[Box<dyn Animal>] is a slice, and a &Vec<Box<dyn Animal>> converts to it automatically.

    Common mistakes

    • Using the vector twice after for animal in animals. This loop takes ownership of the vector, and a second loop over animals fails with error[E0382]: use of moved value: `animals`. Loop over &animals to borrow instead.
    • Forgetting the Box. Vec<dyn Animal> is not allowed, because dyn Animal has no fixed size. A trait object lives behind a pointer: Box<dyn Animal>, &dyn Animal or similar.
    • Omitting the type annotation. Without Vec<Box<dyn Animal>> on the left, Rust sees a Box<Dog> first and rejects the Box<Cat> after it.
    • Writing Box::new(Dog()). A unit struct is a value, not a function, so it is Dog.
    • Expecting dyn to be free. A trait-object call goes through the method table, a small cost per call. When every value has the same known type, a generic function avoids it.

    Key takeaways

    • Polymorphism lets one call site run different code depending on the value's real type.
    • Generics give static dispatch; dyn Trait gives dynamic dispatch.
    • To mix different types in one collection, store trait objects such as Box<dyn Animal>.
    • New types that implement the trait work with existing code unchanged.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free