Traits: Contracts, Not Hierarchies
Rust has no inheritance. No struct extends another struct, there is no base
class, no super, no overriding, and no protected members. For a programmer
arriving from Java, C# or Python this is the most disorienting fact about the
language, and the usual first reaction is to ask how anything gets reused.
The answer begins by noticing that inheritance was never one feature. It was four features that happened to share a keyword.
It provided subtyping: a Dog can be used where an Animal is expected. It
provided code reuse: Dog gets Animal’s method bodies without rewriting
them. It provided interface conformance: Dog is guaranteed to have everything
Animal has. And it provided a shared construction path: Dog’s constructor
runs Animal’s first.
Bundling those was convenient and it is why the fragile base class problem exists, why “prefer composition over inheritance” has been standard advice since 1994, and why every serious object-oriented codebase eventually contains a class hierarchy that nobody dares change. Rust unbundles them. Interface conformance goes to traits. Code reuse goes to composition, default methods and generics. Construction goes to ordinary functions. Subtyping mostly disappears, and where it is genuinely wanted, trait objects provide it.
This chapter is about the trait, which carries the largest share of the load.
A trait is a set of capabilities
A trait names some operations. A type declares it has them by implementing the trait:
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &'static str;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> &'static str {
"circle"
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> &'static str {
"rectangle"
}
}
fn report(shape: &impl Shape) {
println!("{} has area {:.2}", shape.name(), shape.area());
}
fn main() {
report(&Circle { radius: 1.0 });
report(&Rectangle { width: 2.0, height: 3.0 });
}
Circle and Rectangle have no relationship to each other and no shared
ancestor. Neither contains any part of the other. What they have in common is
that both satisfy Shape, and report asks for that and nothing more.
The difference from an interface in Java or C# is smaller than the difference from a base class, but there are two, and both matter.
The first is that implementations are written separately from the type, which means you can implement your own trait for a type you did not define. This is how the standard library’s types acquire behavior from your crate:
trait Pluralize {
fn pluralize(&self, count: usize) -> String;
}
impl Pluralize for str {
fn pluralize(&self, count: usize) -> String {
if count == 1 {
format!("1 {self}")
} else {
format!("{count} {self}s")
}
}
}
fn main() {
println!("{}", "invoice".pluralize(1));
println!("{}", "invoice".pluralize(4));
}
str is defined in the standard library and now has a method defined in this
crate. No wrapper, no static helper, no extension-method ceremony.
The second difference is default methods, which let a trait supply behavior rather than only requiring it.
Default methods are where reuse went
A trait method may have a body. Implementors get it for free and may override it:
trait Greeter {
fn name(&self) -> String;
fn greeting(&self) -> String {
format!("Hello, {}!", self.name())
}
fn shout(&self) -> String {
self.greeting().to_uppercase()
}
}
struct Person {
first: String,
}
impl Greeter for Person {
fn name(&self) -> String {
self.first.clone()
}
}
struct Robot;
impl Greeter for Robot {
fn name(&self) -> String {
String::from("unit 7")
}
fn greeting(&self) -> String {
format!("GREETINGS {}", self.name())
}
}
fn main() {
println!("{}", Person { first: String::from("Ada") }.shout());
println!("{}", Robot.shout());
}
Person implements one method and receives two. Robot overrides one of the
defaults and the other default picks up the override, because shout calls
greeting through the trait.
That last sentence describes the template method pattern, which in an object-oriented language requires an abstract base class with a final method calling abstract ones. Here it is a trait with a default method calling a required one, and it carries none of inheritance’s baggage: no shared fields, no constructor chain, no fragile base class, and a type can do this with as many traits as it likes.
Notice what a trait cannot do, because it is a real limitation. A trait has
no fields. It can require methods, including accessor methods, but it cannot
say “every implementor has a name: String”. Shared data is not what traits
are for. Chapter 11 takes that up.
The orphan rule
There is one restriction on implementing traits, and it exists to prevent a
specific disaster. You may write impl Trait for Type only if the trait is
yours, or the type is yours, or both. Implementing somebody else’s trait for
somebody else’s type is forbidden.
The reason is coherence. If two crates could both write impl Display for Vec<u8>, a program depending on both would have two answers for the same method
call, and which one it got would depend on link order. Every language with
after-the-fact extension has to deal with this; Scala uses scoped implicits, C#
resolves at compile time by using directive, and Rust’s answer is that exactly
one crate can own any given trait-type pairing, decided by a rule that requires
no configuration.
When you need to implement a foreign trait for a foreign type, the newtype from chapter 5 is the way through:
use std::fmt;
struct Csv(Vec<String>);
impl fmt::Display for Csv {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.join(","))
}
}
fn main() {
println!("{}", Csv(vec![String::from("a"), String::from("b")]));
}
Csv is your type, so you may implement anything for it.
The standard traits are a shared vocabulary
Rust’s libraries interoperate to an unusual degree, and the reason is that the standard library defines a small set of traits that everything agrees to speak. Learning them is most of learning to write idiomatic Rust, because implementing the right one gives your type access to machinery written by people who never heard of it.
Debug and Display are the two formatting traits: Debug for programmers,
derived, shown by {:?}, and Display for users, written by hand, shown by
{}. Any type with Display gets to_string free.
From and Into are the conversion traits, and the convention is to implement
From; Into comes automatically through a blanket implementation. This is the
trait ? uses to convert error types, and implementing From<io::Error> for
your error enum is what lets ? work across a library boundary.
Default supplies a zero-argument constructor and is what unwrap_or_default
and struct update syntax rely on.
PartialEq, Eq, PartialOrd and Ord are equality and ordering. Hash makes
a type usable as a map key. Deriving these together is so common that a type
meant to be a key usually derives all of them at once.
Iterator is the largest of them, and implementing its single required method
grants about seventy adapters. Chapter 13 is about that.
AsRef and Borrow express “can be viewed as”, letting a function
accept String, &str, PathBuf and &Path through one parameter.
Send and Sync are thread-safety markers the compiler implements for you.
Chapter 14 is about what they mean.
Deref deserves a warning. It makes one type usable as another by automatic
dereferencing, which is how String gets all of &str’s methods. It is designed
for smart pointers — Box, Rc, MutexGuard — and using it on a newtype to
inherit the inner type’s methods is a documented anti-pattern. It produces
something that looks like inheritance, behaves like inheritance in the good case,
and fails confusingly in the bad one, because method resolution and trait
resolution do not follow the same path.
Coming from Java or C#
FromandIntoreplace conversion constructors and cast operators.DisplayreplacestoString.PartialEqreplacesequals, with the difference that the compiler writes it and that there is no inherited default comparing identity.Defaultreplaces the no-argument constructor. The pattern to notice is that each of these is a separate opt-in, so a type never silently inherits a wrong implementation of anything.
Bounds: asking for capability
A generic function says which capabilities it needs, and the compiler checks that callers supply them:
use std::fmt::Display;
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut best = items[0];
for &item in items {
if item > best {
best = item;
}
}
best
}
fn announce<T>(label: &str, value: T)
where
T: Display,
{
println!("{label}: {value}");
}
fn main() {
println!("{}", largest(&[3, 7, 2]));
println!("{}", largest(&[2.5, 1.0]));
announce("total", 42);
announce("name", "anderix");
}
T: PartialOrd + Copy is a contract: any type that can be compared and copied
may be passed. The where clause is the same thing with better formatting for
longer lists, and is the conventional form once there is more than one bound.
Read a bound as a requirement rather than as a category. T: Display does not
say that T is a kind of displayable thing in some hierarchy; it says the
function will call fmt on it and needs that to exist. This is why bounds
compose so freely — asking for three capabilities is just asking for three
things, and there is no diamond to resolve.
Supertraits are not inheritance
A trait can require another trait:
use std::fmt::Display;
trait Loggable: Display {
fn log_level(&self) -> &'static str;
fn log_line(&self) -> String {
format!("[{}] {}", self.log_level(), self)
}
}
struct Alert(String);
impl Display for Alert {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Loggable for Alert {
fn log_level(&self) -> &'static str {
"WARN"
}
}
fn main() {
println!("{}", Alert(String::from("disk almost full")).log_line());
}
Loggable: Display means an implementor must also implement Display, which
lets log_line use {} on self. It looks like extends and is doing
something different: it composes requirements rather than inheriting
implementation, Alert had to write its own Display, and no data is shared.
Supertraits are how you build a small vocabulary of capabilities into a larger
one, and they do not produce a hierarchy of types — only of requirements.
Designing a trait
Two questions decide whether a trait is any good.
Who implements it, and how many of them are there? A trait with one implementor is usually not earning anything; the abstraction was introduced because it felt like good practice, and it costs a layer of indirection for nothing. Traits pay when there are several implementors, or when there is one now and a plugin boundary where others will arrive, or when a test needs a stand-in.
Is it one capability or several? Small traits compose and large ones do not. A
Storage trait requiring twelve methods forces every implementor to supply
twelve, including the ones they have no use for, which produces
unimplemented!() bodies and a contract that lies. Splitting it into Read,
Write and Seek — which is what the standard library did — lets a type
implement what it can actually do and lets a function ask for what it actually
needs.
The test for the second question is to look at the functions that take the trait as a bound. If most of them need two of the twelve methods, the trait is too big.
Conclusion
Inheritance bundled subtyping, code reuse, interface conformance and construction. Rust separates them, and the trait takes interface conformance plus a meaningful share of code reuse through default methods. What a trait cannot do is share fields, and that is composition’s job.
Implementations are written separately from types, so behavior can be added to foreign types, bounded only by the orphan rule that keeps any trait-type pairing owned by one crate. The standard traits form a vocabulary that makes unrelated libraries interoperate, and implementing the right ones is most of what makes a type feel native.
A bound is a requirement, not a classification, so capabilities compose without hierarchy. The next chapter is about what the compiler does with those bounds — the choice between generating specialized code for each type and dispatching through a pointer at runtime, and what that choice means for a design.
Questions to think through
Take an abstract base class from a system you know. Separate what it provides into the four jobs named at the start of this chapter. Which of the four was the reason it exists, and which came along for the ride?
The chapter says a trait with one implementor is usually not earning anything. Find an interface in your own code with exactly one implementing class. What would be lost by deleting it?
Deref makes one type usable as another, and using it for reuse is an
anti-pattern. Why would method resolution through Deref be a worse foundation
for reuse than a field and a few delegating methods?