In this lesson, you will learn how to abstract over different types using trait objects while keeping the Rust compiler’s strict ownership guarantees intact.
The Problem: When Types Get in the Way
Usually, Rust generics (like Vec<T>) use static dispatch. The compiler creates a unique version of your code for every specific type you use. While fast, this forces you to know the exact type at compile time. Sometimes, you want a collection of different types that all implement the same behavior (like a Shape trait), but you don’t want the collection to be locked to one specific type.
The Solution: Trait Objects
When you use a trait object (e.g., &dyn Trait or Box<dyn Trait>), you are performing type erasure. You are essentially telling the compiler: "I don't care about the concrete type anymore, as long as it implements this trait."
Because the specific size of the underlying type is now hidden (erased), Rust puts the object behind a pointer. The beauty of this is that the Ownership Model still applies perfectly. If you own a Box<dyn Shape>, you own the data behind that pointer. When the Box goes out of scope, the destructor for the underlying concrete type is called, and the memory is freed correctly.
Example
trait Speak {
fn say(&self);
}
struct Dog;
struct Cat;
impl Speak for Dog { fn say(&self) { println!("Woof!"); } }
impl Speak for Cat { fn say(&self) { println!("Meow!"); } }
fn main() {
// We can store different types in one vector by erasing their type
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for animal in animals {
// The compiler knows the 'Speak' trait, but the specific
// type is erased. Ownership is still fully tracked!
animal.say();
}
} // 'animals' goes out of scope, and memory for Dog and Cat is cleaned up.
By erasing the concrete type, we gain flexibility, but because we are using Box, we maintain the single-owner contract. The Box manages the memory regardless of whether the erased type was a Dog or a Cat.
Key Takeaway: Type erasure through trait objects allows you to handle heterogeneous types uniformly without sacrificing memory safety or ownership transparency.