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 implementsAnimal, 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
- Declare the vector in
main_functionwith the typeVec<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. - Fill it with
vec!fromBox::new(Dog)andBox::new(Cat). Each box is converted toBox<dyn Animal>automatically because the annotation says that is what the vector holds.DogandCatare unit structs, so their values are written as just the name. - Loop over the vector with
for animal in animalsand callanimal.speak(). Each box is visited in order, and the call runsDog::speakfor the first andCat::speakfor 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 overanimalsfails witherror[E0382]: use of moved value: `animals`. Loop over&animalsto borrow instead. - Forgetting the
Box.Vec<dyn Animal>is not allowed, becausedyn Animalhas no fixed size. A trait object lives behind a pointer:Box<dyn Animal>,&dyn Animalor similar. - Omitting the type annotation. Without
Vec<Box<dyn Animal>>on the left, Rust sees aBox<Dog>first and rejects theBox<Cat>after it. - Writing
Box::new(Dog()). A unit struct is a value, not a function, so it isDog. - Expecting
dynto 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 Traitgives 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.
