Everyone arriving from an object-oriented language has an inheritance tree on their hard drive they’d love to burn. Person begets Employee, Employee begets Manager — and then one day you add a field to the base class and watch it ripple through forty subclass constructors; or you need something that “serializes and logs” and start agonizing over whose child it should be. Inheritance welds “what something is” to “what something can do” onto a single-rooted tree, and real-world capabilities have never been distributed along a tree.
Part 4 answered “what data is”; this part answers “what data can do.” Rust’s answer is the trait: a contract of behavior that any type can sign, as many times as it likes, independently — no parents, no diamond problem, no base class you can accidentally break. From this part on, your code starts to look like Rust — and eighty percent of that is traits.
A trait is a contract, not a tree
The syntax is plain to the point of being boring:
trait Summary {
fn summarize(&self) -> String; // required
fn preview(&self) -> String { // default, overridable
format!("{}…", &self.summarize()[..50])
}
}
impl Summary for Report {
fn summarize(&self) -> String {
format!("{} ({} words)", self.title, self.word_count)
}
}
The type Report signs the contract Summary: it provides summarize and gets the default preview for free. Notice the signing happens in an impl block, not in the type definition — behavior is attached afterward, not inherited at birth. That single distinction produces the trait’s most important property: you can implement your traits for other people’s types. Implement Summary for the standard library’s Vec<T>? Legal. Implement someone else’s trait for your Report? Also legal.
The one thing you cannot do is implement someone else’s trait for someone else’s type — that’s the orphan rule: in any impl, either the trait or the type must be local to your crate. It looks like a restriction; it’s actually a guard rail. Without it, two dependencies could each implement the same trait for Vec<String>, and your build result would depend on link order — not a bug anyone wants to meet in production. The idiomatic escape is the newtype pattern: wrap it in struct MyVec(Vec<String>), the type is now yours, and the rule is satisfied.
A world without inheritance organizes by composition
The real disease of the inheritance tree isn’t depth — it’s that it forces every type to choose exactly one parent, so “what it can do” gets disguised as “what it is.” Traits tear the disguise off: a capability is a capability, and a type signs as many as it wants.
Compare the two mental models:
- Inheritance asks: “Whose child am I?” — one answer only; change the top, shake the bottom; the diamond problem has haunted language designers for decades.
- Traits ask: “Which contracts have I signed?” — a list, each contract oblivious to the others; add a capability by adding an impl block.
Generic functions consume that list just as directly. fn show(x: &impl Summary) reads as “anything that signed Summary” — the function doesn’t care what the value is, only what it can do. That’s why Rust has no inheritance and never misses it: inheritance tried to solve “reuse implementation” and “express interfaces” with one tree and did both awkwardly; traits handle only interfaces, and reuse goes to composition — shared logic lives in plain fields and plain functions held by the types that need them. Two problems, each solved cleanly.
And then there’s the free lunch sold by one line — #[derive(...)]:
#[derive(Debug, Clone, PartialEq)]
struct Report { title: String, word_count: usize }
The compiler generates the implementation mechanically from your fields. Debug, Clone, PartialEq, Eq, Hash, Default — the traits that involve “no surprises, only manual labor” are standard practice to derive; save the handwriting for the ones that need judgment.
The six standard traits you’ll touch every day
The most-used traits in the standard library fit on one hand, and together they form the ground-level grammar of “idiomatic Rust”:
DebugvsDisplay— two faces of “turn into a string.”Debugis for programmers ({:?}, in logs and assertions) and should be derived, always;Displayis for users ({}) and must be written by hand — what counts as “nice to look at” is a judgment call no machine can make for you. ImplementDisplayandToStringarrives automatically — that’s the compound interest of the trait system: sign one contract, unlock a chain of associated capabilities.From/Into— conversions with a name. This is what?used in Part 4 to convert errors. The convention: implementFrom, getIntofor free — signString: From<&str>and"hi".into()works.Frompromises the conversion cannot fail; fallible conversions go throughTryFrom.Clone— explicit copies. In Rust, copying is always written out: no implicit copy constructor quietly heap-allocating in the dark.Clonemakes.clone()an honest statement of “I’m paying for a second ownership here.” (Copyis the special case: small types where a bitwise copy suffices, like numbers — copies happen implicitly because they cost nothing.)Drop— deterministic cleanup. Runs automatically when a value leaves scope: files close, locks release, connections return to the pool. This is RAII as Rust knows it, and the extension point behind Part 2’s sentence “when the owner goes away, the value is cleaned up.” You’ll rarely write it by hand, but understanding it explains why Rust’s resource management needs nofinally.Iterator— one method unlocks an entire pipeline. The most profitable contract of the six, and worth its own section.
Iterator: sign next(), get the whole ecosystem
The entire obligation of the Iterator trait is a single method: fn next(&mut self) -> Option<Self::Item> — either hand over the next element or say None. And the standard library has built seventy-some adapters on top of it. This is the best exhibit of trait design power: a tiny contract, enormous compound interest.
let squares: Vec<u32> = (1..=100)
.filter(|n| n % 2 == 0)
.map(|n| n * n)
.take(3)
.collect(); // [4, 16, 36]
Two properties make it more than syntactic sugar:
- Laziness. Adapters like
filter,map, andtakedo nothing — no allocation, no iteration; they just assemble a plan. Only when a consumer likecollect(orsum, or aforloop) starts pulling do values flow through the pipeline, one at a time. No intermediate collections, no wasted passes. - Zero cost. The whole chain compiles down to the same assembly as the hand-written loop you’d have written — the most literal fulfillment of the “zero-cost abstraction” promise. Write more declarative code, pay nothing at runtime.
And the contract is symmetric: implement next() for your own type and it instantly plugs into all seventy adapters — map, filter, zip, fold, collect, all free. In the inheritance world, that would mean extending some giant AbstractCollection; in the trait world, you signed one line of obligation.
Practice, then Part 6
- Implement
Displayfor Part 4’sConnection: print the session id forConnected, the reason forFailed. Then deriveDebugand print the same value with{}and{:?}— feel what the two faces, “for users” and “for you,” should each look like. - Write
trait Area { fn area(&self) -> f64; }, implement it forCircleandRect, then writefn total_area(shapes: &[impl Area]) -> f64. Now swapimpl Areafor the full generic form<T: Area>that Part 7 will cover in depth — confirm both compile to the same behavior and file that equivalence into muscle memory. - Without a for loop, using only iterator adapters: given a vector of log lines, sum the byte length of all lines containing
"ERROR". Then try “group the lines in threes” — hint:chunksis not anIteratoradapter but a slice method; find it in the docs. Today is the day you learn where the answers live.
Part 6 returns to ownership, this time carrying escape hatches: Box, Rc, RefCell — how smart pointers move borrow checking from compile time to runtime, when that trade is worth making, and where the “single ownership” rule’s real elastic boundary lies. After it, you’ll know that behind every “no” the borrow checker says, there’s a clearly priced door.