Sling Academy
Home/Rust/How Rust Differs from Classical OOP: Ownership Instead of Inheritance

How Rust Differs from Classical OOP: Ownership Instead of Inheritance

Last updated: January 06, 2025

In object-oriented programming (OOP), inheritance is a key concept often used to promote reusability and organization. In classical OOP languages like Java and C++, classes use inheritance to define hierarchies, allowing one class (subclass) to inherit properties and behavior from another (superclass). However, this can introduce complexity, tightly-coupled code, and often leads to the infamous "diamond problem." Rust, a systems programming language, approaches code reuse and safety differently with its unique concept of “ownership” instead of classical inheritance.

Understanding Ownership in Rust

Rust’s ownership model is its most distinct feature. It governs memory safety through a set of rules enforced by the compiler, making rust code safe from issues such as data races, dangling pointers, or buffer overflows. Here are the three main rules of ownership in Rust:

  • Each value in Rust has an owner.
  • There can be only one owner at a time.
  • When the owner goes out of scope, the value is dropped.

Instead of relying on object hierarchies to manage data and operations, developers use Rust's ownership, borrowing, and lifetimes:

Ownership by Example

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is no longer valid

    // println!("{}", s1); // Error! Can't use s1 anymore
    println!("{}", s2);   // Works fine
}

In the example, s1 is moved to s2 rather than being copied. Once s1 is moved, it is no longer valid and cannot be used, demonstrating the unique ownership model.

Inheritance-like Patterns in Rust

While Rust does not support inheritance as understood in classical OOP, it offers several idioms that achieve similar code sharing and reuse:

Traits

Traits are Rust’s way of defining shared behavior. They allow grouping of method signatures to be implemented by different types. This is similar to interfaces in languages like Java.

trait Greet {
    fn greet(&self);
}

struct Person;

impl Greet for Person {
    fn greet(&self) {
        println!("Hello!");
    }
}

fn main() {
    let p = Person;
    p.greet();
}

This code defines a Greet trait which can be associated with any type to implement the greet method.

Trait Objects and Dynamic Dispatch

Similar to polymorphism, trait objects in Rust are used to call methods on types via pointers of different types at runtime.

fn show_greet(g: &dyn Greet) {
    g.greet();
}

fn main() {
    let p = Person;
    show_greet(&p);
}

This permits passing around instances that conform to a trait, achieving a behavior similar to polymorphic inheritance without needing class hierarchies.

Composition Over Inheritance

Rust emphasizes composition over inheritance—a principle where types hold instances of other types to delegate responsibility. This leads to more modular, flexible, and maintainable code:

struct Engine;

struct Car {
    engine: Engine,
}

fn main() {
    let my_car = Car { engine: Engine };
    // Operations using `my_car.engine`
}

This highlights how functionality can be encapsulated as a part of another struct, without overlapping concerns found in inheritance hierarchies.

Conclusion

While inheritance is a core part of classical OOP languages, Rust uses ownership as a central feature to provide safety and performance without needing explicit memory management or the complexity of inheritance. Traits and composition offer flexible mechanisms for sharing behavior across different types, promoting modular design that helps avoid the pitfalls of deep inheritance hierarchies. Thus, Rust provides a different, yet powerful approach to software design, compelling us to reconsider traditional paradigms in favor of Rust’s safer abstractions.

Next Article: Emulating Inheritance with Trait Composition in Rust

Previous Article: Using Traits in Rust for Polymorphic Behavior

Series: Object-Oriented Programming in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior