Start learning free

LESSON 403 OF 414 · OBJECT-ORIENTED PROGRAMMING

Inheritance in Rust: Composition and Delegation

Lesson Read the full explanation

TL ; DR:

In many languages a child class inherits from a parent class and gets its fields and methods for free. Rust has no inheritance: a struct reuses another type by composition, holding it as a field and forwarding calls to it. This lets you reuse code instead of copy-pasting it.


struct Puppy {
    dog: Dog,
}
impl Puppy {
    fn new(name: &str) -> Puppy {
        Puppy { dog: Dog { name: name.to_string() } }
    }
    fn whine(&self) { println!("whimper"); }
}


Full lesson:

Inheritance is one of the four classic OOP pillars (along with encapsulation, polymorphism, and abstraction). The idea is simple: most real-world things are kinds of other things — a Puppy is a Dog, a Car is a Vehicle, a Manager is an Employee. In languages with classes, the child class gets the parent's data and behavior automatically. Rust has no class inheritance; instead a Puppy has a Dog inside it, and you reuse the Dog's data and behavior through that field.

What gets reused?

  • Fields — the Puppy holds a Dog in its dog field, so the name is reachable as self.dog.name even though we only declared it on Dog.
  • Methods — the Puppy gets a bark() method that forwards to self.dog.bark(); methods are not promoted automatically.
  • The contract — Rust doesn't let you pass a Puppy where a Dog is expected; shared behavior is expressed with traits instead (we'll see this when we cover polymorphism).

What you still write

  • The constructor Puppy::new has to build the inner Dog itself (because the Dog's fields need initializing).
  • Any new methods or new fields specific to the puppy.

Your task

Below the Dog struct, create a Puppy struct that wraps a Dog, with a new function, a bark() that forwards to the dog, and one new method, whine(), that prints "whimper". The code in main_function creates a Puppy named "Buddy", calls its forwarding bark(), and then its new whine().

Expected output:

Woof! I am Buddy
whimper
STEP 1 OF 8
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING

    Inheritance in Rust, explained

    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

    1. Declare struct Puppy with a field dog: Dog. This is the whole "inheritance" relationship.
    2. Open impl Puppy. Methods and associated functions live in impl blocks, never inside the struct body.
    3. Write a new function that takes name: &str and returns Self. There are no inherited constructors in Rust, so Puppy must say how to build itself. Self is an alias for Puppy inside the block.
    4. Build the inner dog. The name field of Dog is an owned String, while the parameter is a borrowed &str. name.to_string() makes the owned copy.
    5. Add bark that delegates. It takes &self and calls self.dog.bark(). This one-line forwarding method is the explicit replacement for the automatic inheritance of methods.
    6. Add whine, which prints whimper. 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 call self.dog.bark() lends the inner dog the same way. Nothing is moved or cloned.
    • The last expression is the value. new has no return and no semicolon after the struct literal, so that literal is what the function returns.
    • Field privacy. Both structs are in one module here, so new can build a Dog directly. From another module the fields would need pub or a constructor on Dog.

    Limits of composition and common mistakes

    • Expecting a Puppy to be accepted where a Dog is expected. A function with a Dog parameter does not accept a Puppy. 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 { } without dog is a compile error.
    • Passing a &str where a String is needed. Dog { name } with name: &str is a type mismatch. Convert with to_string(), String::from or to_owned().
    • Calling the wrong bark. If Puppy has its own bark, buddy.bark() runs that one, and buddy.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.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free