In Rust, a systems programming language known for safety and performance, inheritance as seen in object-oriented programming languages does not exist. Instead, Rust focuses on composition over inheritance. By composing structs and leveraging traits, developers can achieve flexible and reusable code similar to what inheritance offers in other languages. This article will discuss how to inherit behavior through the composition of Rust structs.
Understanding Composition
Composition is a design principle where you build complex types by combining objects. In Rust, you create structures that contain other structures as fields. This allows you to reuse the behavior of contained structures with efficacy.
Defining Structs
Let's begin by defining simple structs in Rust. We will use these to build more complex behaviors:
struct Engine {
power: u32,
}
struct Wheels {
count: u8,
}
struct Car {
engine: Engine,
wheels: Wheels,
}
Here, we define two basic structs, Engine and Wheels. The Car struct composes these two to represent a more complex entity.
Implementing Behavior
Each struct can implement its methods to behave in specific ways. Let's add some methods:
impl Engine {
fn start(&self) -> String {
format!("Engine with {} power is starting.", self.power)
}
}
impl Wheels {
fn roll(&self) -> String {
format!("{} wheels are rolling.", self.count)
}
}
Now, we add the behavior to the Car struct by creating methods that utilize its fields:
impl Car {
fn start_car(&self) -> String {
self.engine.start()
}
fn drive(&self) -> String {
self.wheels.roll()
}
}
Here, the Car struct effectively inherits behavior from both Engine and Wheels through composition, as it calls methods of its owned structs.
The Role of Traits
To create a common interface among multiple structs, Rust uses traits, akin to interfaces or abstract base classes in other languages:
trait Vehicle {
fn vehicle_info(&self) -> String;
}
impl Vehicle for Car {
fn vehicle_info(&self) -> String {
format!("Car with engine power {} and {} wheels.", self.engine.power, self.wheels.count)
}
}
Traits allow structs like Car to guarantee certain behaviors and provide polymorphic capabilities, enhancing modular interfaces without subclassing.
Benefits Over Inheritance
Composition in Rust helps avoid the problems associated with inheritance, such as the fragility of a base class possibly breaking extended classes when changed. By relying on composition, you focus on simpler, self-contained units that are easier to test and debug.
Practical Example
Consider the need to model an electric car in addition to our gas-fueled Car. Rather than worrying about subclassing, you directly compose a new struct:
struct Battery {
capacity: u32,
}
struct ElectricCar {
engine: Engine,
wheels: Wheels,
battery: Battery,
}
impl ElectricCar {
fn charge(&self) -> String {
format!("Battery capacity: {} is charging.", self.battery.capacity)
}
}
The ElectricCar struct is independent in its implementation, focusing on extending its behavior without worrying about limitations from superclass design.
Conclusion
By using composition and traits, Rust developers achieve effective code reuse and conceptual inheritance. Structs hold each other's components, and behavior is defined in terms of these integrations, leading to flexible and expressive code bases that embrace Rust’s strengths in safety and concurrency.