Object-Oriented Programming in Rust
Object-oriented programming (OOP) is defined by three core characteristics: objects, encapsulation, and inheritance. While Rust is a multi-paradigm language influenced by functional programming, it approaches these concepts differently than traditional OOP languages like Java or C++. This chapter examines how Rust supports objects and encapsulation, why it rejects classical inheritance, and how it achieves code reuse and polymorphism using traits and generics.
1. Defining Object-Oriented Programming and Rust's Stance
There is no universal consensus on what constitutes an object-oriented language. However, most definitions center around three pillars:
- Objects: Data combined with behavior operating on that data.
- Encapsulation: Hiding internal implementation details behind a public API.
- Inheritance: Deriving a new object's structure and behavior from an existing object's definition.
Rust adopts the first two characteristics fully while completely replacing the third with a system built on structs, enums, implementation blocks, and traits.
2. Objects in Rust: Structs and Enums
In classical OOP languages, objects bundle data and methods together inside a class definition. In Rust, this separation of concerns is handled explicitly:
- Data Containers:
structandenumtypes hold state (data). - Behavior Providers:
impl(implementation) blocks define methods associated with those structs and enums.
// Defining data structure (State)
struct Point {
x: f64,
y: f64,
}
// Defining behavior (Methods)
impl Point {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Even though Rust does not call them "objects," structs and enums with implementation blocks provide the exact same runtime functionality as objects.
3. Encapsulation: Hiding Implementation Details
Encapsulation ensures that code outside an object cannot inspect or modify its internals directly. This decouples internal implementation changes from external consumer code.
Visibility Rules in Rust
- Private by Default: In Rust, all items (modules, types, functions, methods, and struct fields) are private by default.
- The
pubKeyword: Developers must explicitly use thepubkeyword to expose types or methods to external crates or modules. Struct fields remain private even if the struct itself is public, unless markedpubindividually.
4. Practical Example: The AverageCollection Struct
To see encapsulation in action, consider a collection that caches its average value. If internal fields were public, external code could modify the vector directly and forget to update the cached average. Rust prevents this by keeping fields private and exposing a controlled API.
pub struct AverageCollection {
list: Vec<i32>,
average: f64,
}
impl AverageCollection {
pub fn new() -> AverageCollection {
AverageCollection {
list: Vec::new(),
average: 0.0,
}
}
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
// Private helper method encapsulated from external callers
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
let count = self.list.len();
if count == 0 {
self.average = 0.0;
} else {
self.average = total as f64 / count as f64;
}
}
}
Note: Because
listandaverageare private, code usingAverageCollectioncannot tamper with internal state directly. The internal storage could later be refactored from aVec<i32>to aHashSet<i32>, and as long as public method signatures remain unchanged, external consumer code requires zero modifications.
5. The Absence of Inheritance in Rust
Inheritance is the ability for an object to inherit another object's definitions, gaining its data and behavior automatically. Rust does not support inheritance. You cannot define a struct that inherits fields and methods from a parent struct.
Developers typically reach for inheritance for two primary reasons:
- Code Sharing: Reusing common behavior across multiple types.
- Polymorphism: Substituting multiple objects for each other at runtime based on shared traits.
Rust provides alternative language features to address both requirements without the tight coupling and fragility associated with deep class hierarchies.
6. Solving Code Sharing: Default Trait Methods
To achieve code sharing without inheritance, Rust uses traits. Traits define shared behavior that types can implement. Furthermore, traits can provide default method implementations.
pub trait Summary {
// Default implementation can be used or overridden by implementing types
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct Article {
pub headline: String,
pub content: String,
}
impl Summary for Article {
// Article uses the default implementation of summarize()
}
pub struct Tweet {
pub username: String,
pub content: String,
}
impl Summary for Tweet {
// Tweet overrides the default implementation
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Note: Traits in Rust can define methods, but as of this language version, traits cannot define instance fields (state). State must always reside inside structs.
7. Solving Polymorphism: Generics vs. Trait Objects
Polymorphism allows different types to be used interchangeably through a common interface. Classical OOP achieves this via parent classes and subclasses (e.g., passing a Truck, Car, or Motorcycle into a function expecting a base Vehicle class).
Rust approaches polymorphism through two distinct mechanisms:
// Static Dispatch using Generics and Trait Bounds
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
8. Summary and Key Takeaways
- Multi-Paradigm Influence: Rust is influenced by object-oriented programming, supporting data encapsulation and objects while omitting classical inheritance.
- Structs and Enums as Objects: Data is stored in
structs andenums, while behavior is attached usingimplblocks. - Encapsulation by Default: All items and struct fields are private by default; the
pubkeyword must be explicitly used to expose a public API. - Decoupled Internals: Encapsulation allows internal data structures (e.g., switching from
VectoHashSet) to be modified without breaking consumer code. - No Classical Inheritance: Rust completely avoids structural inheritance, eliminating complex hierarchies and fragile base class problems.
- Code Sharing via Traits: Traits allow common behavior to be shared across types using default method implementations.
- Polymorphism via Generics and Trait Objects: Rust achieves polymorphism at compile-time via static dispatch (generics) or at runtime via dynamic dispatch (trait objects).