Inheritance and what Rust does instead
Inheritance is the idea that one type is a more specific kind of another: a puppy is a dog, so a puppy should have the dog's data and behaviour without repeating it. Languages like Java and Python build this into the language with a parent class and a child class.
Rust has no class inheritance. A struct cannot extend another struct and does not receive its fields or methods. Rust gets the same reuse in two other ways:
- Composition. A struct stores another struct as a field and uses it. This is the subject of this lesson.
- Traits. Shared behaviour is written once as a trait, possibly with default method bodies, and any number of types implement it. The following lessons on overriding, polymorphism and abstract types use this.
The practical rule is "has a" instead of "is a": a Puppy has a Dog inside it.
Composition: a Puppy that wraps a Dog
The lesson starts with a Dog that has a name and a bark method:
struct Dog {
name: String,
}
impl Dog {
fn bark(&self) {
println!("Woof! I am {}", self.name);
}
}
A Puppy stores a Dog by value in a field. There is no pointer or box here: the dog's bytes sit inside the puppy's, so the whole thing is one value.
struct Puppy {
dog: Dog,
}
Because nothing is inherited, the compiler does not let Puppy call bark on its own. Calling p.bark() on a Puppy that has no such method fails with error[E0599]: no method named `bark` found for struct `Puppy` in the current scope, and the compiler suggests p.dog.bark(). Reaching through the field always works, but it exposes the inner structure to every caller.
Step by step
- Declare
struct Puppywith a fielddog: Dog. This is the whole "inheritance" relationship. - Open
impl Puppy. Methods and associated functions live inimplblocks, never inside the struct body. - Write a
newfunction that takesname: &strand returnsSelf. There are no inherited constructors in Rust, soPuppymust say how to build itself.Selfis an alias forPuppyinside the block. - Build the inner dog. The
namefield ofDogis an ownedString, while the parameter is a borrowed&str.name.to_string()makes the owned copy. - Add
barkthat delegates. It takes&selfand callsself.dog.bark(). This one-line forwarding method is the explicit replacement for the automatic inheritance of methods. - Add
whine, which printswhimper. It is new behaviour that only puppies have.
The solution in Rust
struct Dog {
name: String,
}
impl Dog {
fn bark(&self) {
println!("Woof! I am {}", self.name);
}
}
struct Puppy {
dog: Dog,
}
impl Puppy {
fn new(name: &str) -> Self {
Self {
dog: Dog {
name: name.to_string(),
},
}
}
fn bark(&self) {
self.dog.bark();
}
fn whine(&self) {
println!("whimper");
}
}
fn main_function() {
let buddy = Puppy::new("Buddy");
buddy.bark();
buddy.whine();
}
The expected output is:
Woof! I am Buddy
whimper
Details worth noticing
- Delegation is explicit. Every dog method you want puppies to expose needs its own forwarding method. That is more typing than inheritance, but it also means a puppy exposes only what you chose to expose.
- Methods borrow.
bark(&self)borrows the puppy immutably, and the callself.dog.bark()lends the inner dog the same way. Nothing is moved or cloned. - The last expression is the value.
newhas noreturnand no semicolon after the struct literal, so that literal is what the function returns. - Field privacy. Both structs are in one module here, so
newcan build aDogdirectly. From another module the fields would needpubor a constructor onDog.
Limits of composition and common mistakes
- Expecting a
Puppyto be accepted where aDogis expected. A function with aDogparameter does not accept aPuppy. They are unrelated types. To write code that accepts both, define a trait and implement it for both types, which later lessons do. - Forgetting to initialise the inner value. A struct literal must set every field, so
Self { }withoutdogis a compile error. - Passing a
&strwhere aStringis needed.Dog { name }withname: &stris a type mismatch. Convert withto_string(),String::fromorto_owned(). - Calling the wrong
bark. IfPuppyhas its ownbark,buddy.bark()runs that one, andbuddy.dog.bark()runs the dog's. The two names do not conflict, because they belong to different types.
Key takeaways
- Rust has no class inheritance; reuse comes from composition and traits.
- A struct holds another struct as a field, and its methods delegate to that field.
- Constructors are ordinary associated functions, usually called
new, and each type writes its own. - Behaviour that several types share is best expressed as a trait.
