Trait Inheritance-Like Bounds
Rust does not have class inheritance where a subclass inherits fields and memory layout from a base class. Instead, Rust models hierarchical capability relationships through trait inheritance-like bounds (trait Subtrait: BaseTrait), where a subtrait declares that every implementing type must also implement one or more base traits.
How Subtrait Bounds Differ from Object-Oriented Inheritance
OOP Class Inheritance (Java/C++) Rust Trait Hierarchy (Subtrait Bounds)
+------------------------------+ +------------------------------------+
| class Base { int id; } | | trait Identifiable { fn id(&self);}|
| class Derived : Base { ... } | | trait Auditable: Identifiable { ..}|
+------------------------------+ +------------------------------------+
- Shares memory layout / fields - Zero shared fields or memory layout
- Implicit subtyping - Explicit prerequisite contract on Self
In Rust, writing trait Auditable: Identifiable is shorthand for placing a where Self: Identifiable constraint on the trait definition itself.
Implementing a Hierarchy of Traits
Each trait in the hierarchy is still implemented in its own separate impl block for a concrete type:
trait Named {
fn name(&self) -> &str;
}
trait Versioned: Named {
fn version(&self) -> (u32, u32, u32);
fn full_identifier(&self) -> String {
let (maj, min, pat) = self.version();
// Can call `self.name()` directly because `Self: Named` is guaranteed:
format!("{} v{}.{}.{}", self.name(), maj, min, pat)
}
}
struct Package {
pkg_name: String,
semver: (u32, u32, u32),
}
// 1. Implement the prerequisite trait:
impl Named for Package {
fn name(&self) -> &str {
&self.pkg_name
}
}
// 2. Implement the subtrait:
impl Versioned for Package {
fn version(&self) -> (u32, u32, u32) {
self.semver
}
}
fn print_package_info<T: Versioned>(item: &T) {
// A generic function bounded only by `T: Versioned` can call methods from BOTH traits:
println!("Name: {}", item.name());
println!("Full: {}", item.full_identifier());
}
fn main() {
let pkg = Package {
pkg_name: String::from("serde"),
semver: (1, 0, 210),
};
print_package_info(&pkg);
}
Name: serde
Full: serde v1.0.210
Trait Alias Pattern via Empty Subtraits + Blanket Implementations
Because stable Rust does not yet have a trait Alias = TraitA + TraitB; keyword, libraries often combine a subtrait bound with a blanket implementation to create a trait alias for a frequently repeated group of bounds:
use std::fmt::{Debug, Display};
// 1. Declare an empty subtrait requiring all component traits:
trait PrimKey: Copy + Ord + std::hash::Hash + Debug + Display {}
// 2. Blanket-implement `PrimKey` automatically for EVERY type satisfying those bounds:
impl<T> PrimKey for T where T: Copy + Ord + std::hash::Hash + Debug + Display {}
fn log_sorted_keys<K: PrimKey>(mut keys: Vec<K>) {
keys.sort();
for k in keys {
println!("Key: {k} ({k:?})");
}
}
fn main() {
log_sorted_keys(vec![42_u32, 7, 19]);
log_sorted_keys(vec!["beta", "alpha", "gamma"]);
}
Key: 7 (7)
Key: 19 (19)
Key: 42 (42)
Key: alpha ("alpha")
Key: beta ("beta")
Key: gamma ("gamma")
| OOP Inheritance Feature | Rust Equivalent |
|---|---|
Interface extends interface (interface B extends A) |
trait B: A (supertrait bound) |
| Shared default behavior across implementors | Default method implementations inside trait |
| Reusing fields from another struct | Struct composition (embedding inner: BaseStruct as a field) |
| Delegating methods to an inner field | Forwarding methods in impl Trait for Outer (or Deref for smart pointers) |