Why doesn't Rust have inheritance? A design perspective

Explore why why doesn't rust have inheritance, how Rust achieves polymorphism with traits, and practical patterns for code reuse through composition and generics. This educational guide covers design decisions, performance considerations, and API design tips for Rust developers.

Corrosion Expert
Corrosion Expert Team
·5 min read
Rust Inheritance Explained - Corrosion Expert
Photo by Pexelsvia Pixabay
Quick AnswerDefinition

Rust does not use classical inheritance by design. Instead, Rust emphasizes composition and trait-based polymorphism to achieve code reuse and safety. This approach avoids the complexity and runtime costs of inheritance, while enabling flexible APIs through trait bounds, generics, and optional dynamic dispatch. In this article we unpack the reasoning and practical patterns you can apply in real projects.

Why Rust avoids classical inheritance

In many object-oriented languages, inheritance provides a shared base class and a runtime dispatch path. Rust does not implement class-based inheritance; instead, it relies on composition and traits to define behavior. This choice reduces runtime cost, avoids the diamond problem, and strengthens module boundaries. Practically, you define small, reusable types and implement traits on them, then compose behavior by combining these pieces. The phrasing why doesn't rust have inheritance comes up frequently for new Rustaceans; the language design deliberately limits inheritance to preserve safety and clarity. The contrast can be seen in the absence of a base class hierarchy and the emphasis on explicit interfaces.

Rust
struct Cat { name: String } impl Cat { fn speak(&self) { println!("meow"); } } struct Dog { name: String } impl Dog { fn speak(&self) { println!("bark"); } }
  • In this simple example, Cat and Dog do not share a common base type providing speak; instead, you model behavior through separate impl blocks.
  • This avoids shared mutable state pitfalls and makes behavior explicit per type.

Why this matters: avoiding inheritance reduces runtime costs and prevents brittle hierarchies, enabling safer composition of capabilities.

Rust
trait Speak { fn speak(&self); } struct Cat2 { name: String } impl Speak for Cat2 { fn speak(&self) { println!("meow"); } } struct Dog2 { name: String } impl Speak for Dog2 { fn speak(&self) { println!("woof"); } }
  • Define a minimal interface, then implement it for concrete types. This is the core of Rust’s approach to polymorphism.

lengthy_code_note_for_section_1_continue

Steps

Estimated time: 60-90 minutes

  1. 1

    Define a shared behavior as a trait

    Create a trait that captures the behavior you want to share across types, rather than a base class. This defines an interface without forcing a single inheritance chain.

    Tip: Keep traits focused and small to maximize reuse and composability.
  2. 2

    Implement the trait on concrete types

    Provide concrete behavior for each type by implementing the trait. This replaces a shared base class with explicit implementations.

    Tip: Use explicit impl blocks to keep behavior localized to each type.
  3. 3

    Use generics for compile-time polymorphism

    Write functions that accept any type implementing the trait, enabling zero-cost abstractions through monomorphization.

    Tip: Prefer generic bounds to avoid dynamic dispatch when possible.
  4. 4

    When dynamic dispatch is needed

    If you need runtime flexibility, use trait objects with Box<dyn Trait> or &dyn Trait.

    Tip: Be mindful of the costs of dynamic dispatch and lifetimes.
  5. 5

    Compose behavior through struct composition

    Assemble complex types by combining smaller components (structs) and delegating behavior via trait implementations.

    Tip: Composition often scales better than deep inheritance trees.
Pro Tip: Prefer trait-based design over inheritance to improve modularity and testability.
Warning: Avoid overusing dynamic dispatch in hot-path code; prefer monomorphized generics when possible.
Note: Lifetimes and trait objects affect API ergonomics—design with clear ownership in mind.

Prerequisites

Required

Commands

ActionCommand
Check Rust versionVerify installed toolchain versionrustc --version
Create a new binary projectSet up a fresh project for experimentscargo new my_app --bin
Build the projectCompile to ensure code correctnesscargo build
Run the binaryExecute the compiled binary in the current projectcargo run
Run testsRun unit/integration testscargo test

Quick Answers

What is inheritance and why is it common in OO languages?

Inheritance is a mechanism where a new type derives behavior from an existing type. It creates a class hierarchy and enables polymorphism through base class references. Rust forgoes this model to avoid runtime costs and ambiguity in complex hierarchies.

Inheritance is when a type inherits behavior from another; Rust avoids this in favor of traits and composition for safer, clearer APIs.

What are traits and how do they replace inheritance in Rust?

Traits define a set of methods a type must implement. They provide a contract similar to an interface and enable polymorphism without a class hierarchy. Types implement traits to gain shared behavior, and functions can be generic over any type implementing a trait.

Traits are like interfaces. They let you share behavior across types without inheritance, enabling flexible and safe polymorphism.

Can Rust still express common inheritance patterns?

Rust replaces inheritance with composition and trait-based design. You can simulate common patterns by combining smaller components and using generic constraints, but you won’t have a single base class. This leads to more explicit API design and often better compile-time checks.

You can get similar outcomes using traits and composition, but Rust won’t provide a traditional base class.

When should I use trait objects vs generics?

Use generics when you want zero-cost abstractions and monomorphization. Use trait objects when you need runtime polymorphism or heterogeneous collections. Each has trade-offs in performance and flexibility.

Choose generics for performance and trait objects for runtime flexibility.

Are there performance costs to not using inheritance?

Not using inheritance avoids vtables and dynamic dispatch costs in many cases, especially with monomorphized generic code. When you need dynamic dispatch, trait objects incur a small runtime cost.

Performance can be excellent with generics; dynamic dispatch adds a small cost when used.

What are practical patterns for API design without inheritance?

Design APIs around small, composable traits, and use generic bounds to share behavior. Favor explicit interfaces and delegation to built components rather than deep hierarchies.

Build APIs with small traits and clear composition for reusable code.

Quick Summary

  • Use traits for shared interfaces
  • Favor composition over inheritance
  • Leverage generics for zero-cost abstraction
  • Reserve dynamic dispatch for runtime flexibility

Related Articles