Consider the following code:
trait Animal { fn make_sound(&self) -> String; } struct Cat; impl Animal for Cat { fn make_sound(&self) -> String { "meow".to_string() } } struct Dog; impl Animal for Dog { fn make_sound(&self) -> String { "woof".to_string() } } fn main () { let dog: Dog = Dog; let cat: Cat = Cat; let v: Vec<Animal> = Vec::new(); v.push(cat); v.push(dog); for animal in v.iter() { println!("{}", animal.make_sound()); } }
The compiler tells me that v
is a vector of Animal
when I try to push cat
(type mismatch)
So, how can I make a vector of objects belonging to a trait and calls the corresponding trait method on each element?
A trait object is an opaque value of another type that implements a set of traits. The set of traits is made up of an object safe base trait plus any number of auto traits. Trait objects implement the base trait, its auto traits, and any supertraits of the base trait.
But trait objects differ from traditional objects in that we can't add data to a trait object. Trait objects aren't as generally useful as objects in other languages: their specific purpose is to allow abstraction across common behavior.
A trait in Rust is a group of methods that are defined for a particular type. Traits are an abstract definition of shared behavior amongst different types. So, in a way, traits are to Rust what interfaces are to Java or abstract classes are to C++. A trait method is able to access other methods within that trait.
In Rust, polymorphic functions are fully type-checked when they are declared, not when they are used. This means you can never call a Rust function and get a type error within the function because you gave it the wrong type.
Vec<Animal>
is not legal, but the compiler can't tell you that because the type mismatch somehow hides it. If we remove the calls to push
, the compiler gives us the following error:
<anon>:22:9: 22:40 error: instantiating a type parameter with an incompatible type `Animal`, which does not fulfill `Sized` [E0144] <anon>:22 let mut v: Vec<Animal> = Vec::new(); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The reason why that's not legal is that a Vec<T>
stores many T
objects consecutively in memory. However, Animal
is a trait, and traits have no size (a Cat
and a Dog
are not guaranteed to have the same size).
To solve this problem, we need to store something that has a size in the Vec
. The most straightforward solution is to wrap the values in a Box
, i.e. Vec<Box<Animal>>
. Box<T>
has a fixed size (a "fat pointer" if T
is a trait, a simple pointer otherwise).
Here's a working main
:
fn main() { let dog: Dog = Dog; let cat: Cat = Cat; let mut v: Vec<Box<Animal>> = Vec::new(); v.push(Box::new(cat)); v.push(Box::new(dog)); for animal in v.iter() { println!("{}", animal.make_sound()); } }
You may use a reference trait object &Animal
to borrow the elements and store these trait objects in a Vec
. You can then enumerate it and use the trait's interface.
Altering the Vec
's generic type by adding a &
in front of the trait will work:
fn main() { let dog: Dog = Dog; let cat: Cat = Cat; let mut v: Vec<&Animal> = Vec::new(); // ~~~~~~~ v.push(&dog); v.push(&cat); for animal in v.iter() { println!("{}", animal.make_sound()); } // Ownership is still bound to the original variable. println!("{}", cat.make_sound()); }
This is great if you may want the original variable to keep ownership and reuse it later.
Keep in mind with the scenario above, you can't transfer ownership of dog
or cat
because the Vec
has borrowed these concrete instances at the same scope.
Introducing a new scope can help handle that particular situation:
fn main() { let dog: Dog = Dog; let cat: Cat = Cat; { let mut v: Vec<&Animal> = Vec::new(); v.push(&dog); v.push(&cat); for animal in v.iter() { println!("{}", animal.make_sound()); } } let pete_dog: Dog = dog; println!("{}", pete_dog.make_sound()); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With