Chapter 9 — Traits
Goal
Name a capability that different concrete types can provide.
A trait is a behavioral contract. Implementations supply the behavior for particular types.
trait Summary {
fn summary(&self) -> String;
}
struct Note {
text: String,
}
fn print_summary(item: &impl Summary) {
println!("{}", item.summary());
}
fn main() {
let note = Note { text: String::from("deploy complete") };
print_summary(¬e);
}
Prediction
What happens?
- A. It prints the note text automatically.
- B. It fails because
Notedoes not implementSummary. - C. It fails because traits cannot return
String. - D. It prints
Noteusing debug formatting.
RevealB
Representative compiler evidence:
error[E0277]: the trait bound `Note: Summary` is not satisfied
|
| print_summary(¬e);
| ------------- ^^^^^ the trait `Summary` is not implemented for `Note`
Defining a trait does not opt types into it. Rust requires an explicit implementation.
Mental model
Working model: a trait says, “any type used here must provide these operations.” A trait bound lets the compiler reject callers that lack them.
Precise model: trait dispatch has two common forms. Generic parameters and impl Trait normally use static dispatch: the compiler knows the concrete type and can generate specialized machine code. A trait object such as &dyn Summary uses dynamic dispatch through runtime metadata and permits different concrete types behind one interface.
Implement the smallest contract:
trait Summary { fn summary(&self) -> String; }
struct Note { text: String }
impl Summary for Note {
fn summary(&self) -> String {
format!("Note: {}", self.text)
}
}
A trait may provide a default method:
struct Note { text: String }
trait Summary {
fn title(&self) -> &str;
fn summary(&self) -> String {
format!("Summary: {}", self.title())
}
}
impl Summary for Note {
fn title(&self) -> &str {
&self.text
}
}
Defaults are useful when the shared behavior is genuinely correct. They are not a substitute for choosing a coherent contract.
Rust's coherence rule prevents arbitrary overlapping implementations. In practical terms, your crate may implement your trait for any suitable type, or an external trait for your local type. It generally cannot implement an external trait for an external type. This keeps trait selection globally predictable.
Simplest fix
Write impl Summary for Note and implement every required method. The compiler then checks the contract at the call site and the implementation site.
Tradeoff alternative: avoid a trait and write an inherent Note::summary method if only Note needs the behavior. That is less machinery and often the right first version. Introduce a trait when code genuinely needs to accept multiple implementations, or when the trait itself carries useful meaning.
Mini challenge
Make both calls compile without changing announce:
trait Named {
fn name(&self) -> &str;
}
struct User(String);
struct Service {
name: String,
}
fn announce(value: &impl Named) {
println!("ready: {}", value.name());
}
fn main() {
announce(&User(String::from("Mira")));
announce(&Service { name: String::from("search") });
}
Answer
trait Named { fn name(&self) -> &str; }
struct User(String);
struct Service { name: String }
impl Named for User {
fn name(&self) -> &str {
&self.0
}
}
impl Named for Service {
fn name(&self) -> &str {
&self.name
}
}
The implementations may retrieve names differently. Callers depend only on the promised behavior.