What the capstone combines
The chapter's earlier lessons each covered one piece of object-oriented design. This exercise uses them together, in Rust's terms:
- A shared contract. A trait
Animalstands in for the abstract parent class. Rust has no inheritance, so each concrete type implements the trait itself. - Several concrete types.
Dog,CatandBirdare separate structs, each with aname: Stringfield. - Per-type behaviour. Each type's
speakprints its own sound. - Polymorphism. A
Vec<Box<dyn Animal>>holds one of each, and a single loop callsspeak()on all of them.
The program must print:
Rex: Woof
Whiskers: Meow
Tweety: Tweet
Approach
The starting file has only the trait, with the single method fn speak(&self);, and an empty main_function. The work splits into two parts: give each animal a type that implements the trait, then build and walk the collection.
The three types are different, so they cannot go in a plain Vec, which holds elements of one type. Boxing each as Box<dyn Animal> turns them into one uniform element type, and each call to speak is dispatched to the right implementation at run time. The order of the vector decides the order of the output, so it must be Rex, Whiskers, Tweety.
Step by step
- Define the three structs, each with one field
name: String. The field is an ownedString, so a struct does not depend on a borrowed string staying alive. - Write an
impl Animal forblock for each. There is nothing to inherit, so every type implementsspeakon its own. The body is aprintln!with a{}placeholder forself.name, followed by the type's sound:Woof,MeoworTweet. The method takes&self, so it reads the field without taking ownership. - Declare the vector in
main_functionasVec<Box<dyn Animal>>, filled withvec!. The annotation is what makes Rust treat each element as a trait object. - Construct each animal inside
Box::new(...). String literals are&str, so each name is converted with.to_string(). - Loop with
for animal in animalsand callanimal.speak().
The solution in Rust
trait Animal {
fn speak(&self);
}
struct Dog {
name: String,
}
impl Animal for Dog {
fn speak(&self) {
println!("{}: Woof", self.name);
}
}
struct Cat {
name: String,
}
impl Animal for Cat {
fn speak(&self) {
println!("{}: Meow", self.name);
}
}
struct Bird {
name: String,
}
impl Animal for Bird {
fn speak(&self) {
println!("{}: Tweet", self.name);
}
}
fn main_function() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog {
name: "Rex".to_string(),
}),
Box::new(Cat {
name: "Whiskers".to_string(),
}),
Box::new(Bird {
name: "Tweety".to_string(),
}),
];
for animal in animals {
animal.speak();
}
}
Details worth noticing
- The loop consumes the vector.
for animal in animalsmoves the boxes out one by one, soanimalscan't be used afterwards. Writefor animal in &animalsto borrow it and keep it. - Adding a type needs no change to the loop. A new
Fishwith its ownimpl Animal for Fishcan be boxed into the vector, and the loop prints its line. - Repetition. The three
speakbodies differ only in the sound, which is a hint that a shared default would help. The next section shows it.
Sharing behaviour with a default method
A fuller version of the design puts the shared logic in the trait. Each type provides only the pieces that differ, name and sound, and speak is written once as a default method:
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> &str;
fn speak(&self) {
println!("{}: {}", self.name(), self.sound());
}
}
struct Dog { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> &str { "Woof" }
}
Cat and Bird follow the same pattern. With Dog named "Rex" and a Cat named "Whiskers", the loop prints Rex: Woof and Whiskers: Meow. The lesson's tests are written for the first version, where the trait has only speak, so submit that one.
Common mistakes
- Passing a
&strto aStringfield.Dog { name: "Rex" }fails witherror[E0308]: mismatched types(expected `String`, found `&str`). Use"Rex".to_string(). - Wrong order in the vector. The output follows the vector, so a different order prints the lines in a different order.
- No
Box, or no type annotation. Without them the vector would try to hold one concrete type, and the second element would be a type mismatch. - Changing the output text. The format is the name, a colon and a space, then the sound. A missing space fails the tests.
- Forgetting the
impl Animal forblock. A struct with a method namedspeakin its ownimplblock is not anAnimaland can't be boxed as one.
Key takeaways
- Rust builds the shape of an OOP design from traits, structs and trait objects instead of class inheritance.
- A trait is the contract, structs carry the data, and
impl Trait for Typesupplies the behaviour. Vec<Box<dyn Trait>>lets one loop work with different types, and new types can be added without changing it.
