A trait is Rust's word for "a named collection of method signatures that any
type can opt into." If you've used Java or C# interfaces, C++ abstract classes
with pure virtual methods, Haskell type classes, Swift protocols, or Python's
abc/Protocol, you already know the gist of it.
The Rust spelling is:
trait Greet {
fn hello(&self) -> String;
}
struct English;
struct German;
impl Greet for English {
fn hello(&self) -> String { "Hello!".to_string() }
}
impl Greet for German {
fn hello(&self) -> String { "Hallo!".to_string() }
}
English and German have nothing in common structurally, but both
"implement Greet." Anywhere code asks for a Greet, either will do.
You've actually been using traits since Chapter 6. Every time you wrote
#[derive(Debug, PartialEq)] on an enum or struct, you were asking the
compiler to write the impl Debug for ... and impl PartialEq for ...
blocks for you. That's all derive is: a macro that emits the obvious
implementation so you don't have to type it out. We'll revisit this in
a moment.
impl Display for Temperature so a value formats itself as "21.5°C" in println!
and format!.Describable trait with two
implementations, plus a generic function with a trait bound that
accepts any T: Describable.dyn Trait). Generics give you one specialized
copy of a function per concrete type ("static dispatch"). Trait
objects give you one function that dispatches at runtime ("dynamic
dispatch") and let you put different types into the same Vec.| Trait | What it gives you | First seen |
|---|---|---|
Debug | {:?} formatting | Chapter 6 (enums) |
Display | {} formatting | this chapter |
PartialEq, Eq | == and != | Chapter 6 |
Clone, Copy | .clone() and implicit copies | Chapter 12 |
Default | T::default() | mentioned in passing |
Iterator | for x in iter, all the combinators | Chapter 16 |
From, Into | T::from(x) and x.into() conversions | sprinkled throughout |
None of those are magic. Each is a regular trait defined in std, and
the standard library impls it for the obvious built-in types. When
you derive one, the compiler writes the impl. When you can't derive
(maybe the auto-generated version isn't what you want), you write
impl Display for MyType { ... } by hand, just like in step 2.
// Static dispatch: the compiler generates a specialized copy of
// `print_all` for each `T` you use it with. Zero runtime cost,
// but every `T` in one call must be the same concrete type.
fn print_all<T: Display>(items: &[T]) { /* ... */ }
// Dynamic dispatch: one function, one vtable lookup per call.
// The slice can mix different concrete types that all implement
// `Display`.
fn print_all_dyn(items: &[&dyn Display]) { /* ... */ }
You'll write both in this chapter. The Box<dyn Trait> form, which
solves the "but trait objects don't have a known size" problem you'll
bump into, is the headline act of Chapter 15 on smart pointers.
Display is the trait behind the {} placeholder in println!,
format!, and friends. Implementing it for your struct means values of
that struct can be formatted as user-facing text the same way a number
or a String can.
The trait lives in std::fmt and looks like this:
pub trait Display {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}
Don't be put off by the signature. In practice you write a one-liner
that delegates to the write! macro, which has the same template
syntax as println! but writes into the formatter:
use std::fmt;
struct Pixel(u8, u8, u8);
impl fmt::Display for Pixel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{:02X}{:02X}{:02X}", self.0, self.1, self.2)
}
}
After that, format!("{}", Pixel(255, 0, 0)) produces "#FF0000".
#[derive(Display)]?Because there's no obvious default. Debug has one (print the type
name and fields), but Display is for human-readable output and
only you know what that should look like for your type. So you write
it by hand. The Java/C# parallel is overriding toString(); the
Python one is __str__.
Useful from the standard library
std::fmt::Displayis the trait.use std::fmt;and thenimpl fmt::Display for Tis the idiomatic spelling.write!is the formatter-targeted cousin ofprintln!. It returnsstd::fmt::Result, which is exactly what yourfmtmethod needs to return, so a singlewrite!(...)call is usually the whole body.- Format specifiers carry over:
{:.1}rounds a float to one decimal place, soformat!("{:.1}", 21.5_f64)is"21.5". You'll want that for the temperature output.
fmt body is one write! call.write! takes the formatter, then a format string, then the args:
write!(f, "{:.1}°C", self.celsius).write! already returns
fmt::Result, so its result is your return value. No semicolon
on the last line, or use an explicit return.impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
Now you're on the other side of the contract. Instead of impling a
trait someone else wrote, you'll write the trait yourself, give two
types their own implementation, and then write a generic function
that accepts anything implementing it.
trait Describable {
fn describe(&self) -> String;
}
That's the entire interface. Any type can opt in by writing
impl Describable for MyType { fn describe(&self) -> String { ... } }.
Once a trait exists, you can use it as a bound on a generic parameter
to say "I accept any T, as long as T implements this trait":
fn print_one<T: Describable>(item: &T) {
println!("{}", item.describe());
}
This is Rust's answer to "polymorphism." The compiler stamps out one
specialized copy of print_one per type you call it with (called
monomorphization; the C++ template crowd will feel at home). There's
no runtime dispatch and no boxing.
A few variations you'll meet in real code, just so you recognize them:
// Multiple bounds with `+`:
fn show<T: Describable + std::fmt::Debug>(item: &T) { /* ... */ }
// Same thing, written with a `where` clause. Easier to read once you
// have several parameters or long bounds:
fn show<T>(item: &T)
where
T: Describable + std::fmt::Debug,
{
/* ... */
}
// `impl Trait` in argument position is shorthand for a single
// unnamed generic parameter:
fn show(item: &impl Describable) { /* ... */ }
You'll write the simple <T: Describable> form in this step. The
others are sugar for the same underlying machinery.
Useful from the standard library
- The Rust Book on traits walks through definitions, implementations, and bounds with more examples than fit here.
Vec<String>::join("\n")(and any&[String].join(...)) is handy for theprint_descriptionsexercise: build aVec<String>of per-item descriptions, then join them with newlines.- The standard
Iterator::mapplus.collect::<Vec<_>>()is the idiomatic way to turn a&[T]into aVec<String>. You met both in passing; Chapter 16 covers iterators properly.
describe methods are one-line format! calls:
format!("{} by {}", self.title, self.author) for Book.format!("{} ({})", self.title, self.year) for Movie.print_descriptions, build a Vec<String> and join it:
let lines: Vec<String> = items.iter().map(|x| x.describe()).collect();
lines.join("\n")
.map/.collect yet (chapter 16
covers them properly), a plain for loop works just as well:
let mut lines: Vec<String> = Vec::new();
for item in items {
lines.push(item.describe());
}
lines.join("\n")
A trait method can ship with a default body. Implementors get that method for free, but any one of them can override it if the default doesn't fit.
trait Greet {
fn name(&self) -> &str;
// Default body. Implementors get this for free.
fn hello(&self) -> String {
format!("Hello, {}!", self.name())
}
}
A type that says impl Greet for X { fn name(&self) -> &str { "world" } }
automatically has .hello() available, returning "Hello, world!",
without writing it out. Provide a fn hello in the impl block and
yours wins instead.
This is how Iterator gets away with offering dozens of methods
(map, filter, sum, count, ...) while only requiring you to
implement one: fn next(&mut self) -> Option<Self::Item>. Every other
method is a default body written in terms of next. Haskellers will
recognise the pattern from type class default methods; Java added the
same feature as "default methods on interfaces" in Java 8.
You'll work with a small Logger trait. There's exactly one required
method (log, which formats a single line) and two default methods
(warn and error) that build on top of it.
trait Logger {
fn log(&self, msg: &str) -> String;
fn warn(&self, msg: &str) -> String {
self.log(&format!("[WARN] {msg}"))
}
fn error(&self, msg: &str) -> String {
self.log(&format!("[ERROR] {msg}"))
}
}
This is deliberately recognisable: every logging library in
every language has this same skeleton. The teaching value of the
default methods is that they live on the trait, so adding a new
implementor doesn't mean writing warn and error from scratch
every time.
Two implementors:
PlainLogger returns the message untouched. It uses both defaults
as written, so all you have to write is log.TaggedLogger { tag: String } prepends a tag (e.g. "auth: ...").
It uses the default warn, but overrides error to swap the
[ERROR] prefix for a louder [CRITICAL] prefix.That asymmetry is the point: implementors take what they want from the defaults and override only the bits they need to change.
Useful from the standard library
- The
format!macro is the workhorse here. Defaultwarnbuilds"[WARN] {msg}"and hands it back toself.log, so whatever decorationlogdoes (the tag, inTaggedLogger's case) wraps the warning prefix.- Default methods are written inside the
traitblock, with a body instead of a trailing semicolon. The required methods (the ones without a body) are still mandatory; defaults are bonus.
PlainLogger::log is one line: msg.to_string(). That's all.
Do not write warn or error for PlainLogger; the defaults
already do the right thing.TaggedLogger::log is also one line: format!("{}: {}", self.tag, msg). The default warn calls this log, so warn("slow")
ends up as "auth: [WARN] slow" automatically.TaggedLogger::error is the override. The pattern mirrors the
default body, just with a different prefix:
fn error(&self, msg: &str) -> String {
self.log(&format!("[CRITICAL] {msg}"))
}
PlainLogger inherits both defaults, TaggedLogger inherits one
and overrides the other. The trait is the only place a default
ever lives, so there's no surprise about where behavior comes
from.The generic print_descriptions<T: Describable> from step 3 is fast
and zero-cost, but it has one limit: every element of a single call
must be the same concrete T. You can pass &[Book] or &[Movie],
but not a slice that contains both.
That's because the compiler picks one T per call site and produces a
specialized copy of the function for it. The slice type &[T] has to
agree on a single element type, and two different structs are two
different types as far as the type system is concerned.
dyn TraitWhen you want a single collection that holds different concrete
types as long as they all implement the same trait, you reach for a
trait object, spelled dyn Trait:
fn run_all(items: &[&dyn Validator], input: &str) {
for v in items {
let _ = v.check(input);
}
}
Two things changed compared to the generic version:
&dyn Validator is a fat pointer: two words
that together point at the value and at a vtable of function
pointers (one per trait method). At each call to .check(...),
Rust looks the function up in the vtable. C++ folks: this is the
same machinery as virtual methods, just opt-in per call site
instead of per class.run_all is compiled exactly once.
The price is the indirection through the vtable; the payoff is
heterogeneous collections.fn f<T: Trait>(x: &T) | fn f(x: &dyn Trait) | |
|---|---|---|
| Dispatch | static, decided at compile time | dynamic, vtable lookup at runtime |
| Code size | one copy per T you use | one copy total |
| Mixed collections | no | yes |
| Runtime cost | none | one indirect call per trait-method call |
Neither is "better." Use generics by default for performance and
flexibility, and reach for dyn Trait when you genuinely need
heterogeneous storage or want a smaller binary.
You'll build a tiny composable validation library. The trait is deliberately small:
trait Validator {
/// `Ok(())` on success, `Err(message)` on failure.
fn check(&self, input: &str) -> Result<(), String>;
}
Three implementors, each enforcing one rule:
MinLength { n }: input must have at least n characters.MustContain { needle }: input must contain the given substring.MustNotContain { forbidden }: input must not contain the
given substring.The killer feature is that MinLength, MustContain, and
MustNotContain are three different types, but a single
&[&dyn Validator] slice can hold all of them at once. That's what
makes "pluggable rules" work: every rule is a different struct (and
might carry different configuration), but the call site only knows
"a list of things that can validate."
You'll see the same idea, scaled up, in Chapter 18's password validator: a configurable set of checks running against one input.
Box<dyn Trait>You'll see Box<dyn Trait> everywhere in real Rust code:
let rules: Vec<Box<dyn Validator>> = vec![
Box::new(MinLength { n: 8 }),
Box::new(MustContain { needle: "@".to_string() }),
];
The reason: dyn Trait has no statically known size (the three
implementors above can carry different fields, so they don't all
take up the same number of bytes), so the compiler won't let you
put bare dyn Validator values directly in a Vec. A Box is a
heap allocation with a fixed-size pointer that lives on the stack,
which sidesteps the size problem. That's exactly what Chapter 15
on smart pointers unpacks. For this step we sidestep it with
&dyn Validator references, which are also fixed-size and work
fine for borrowing.
Useful from the standard library
- The Rust Book on trait objects.
str::contains(with a&strargument) is all you need for theMustContain/MustNotContainchecks.- Inside
collect_errors, a plainforloop pushing into aVec<String>is the most direct form. The same chain with.iter().filter_map(...)works once you've met iterators in Chapter 16.
check impls follow MinLength exactly. Use
input.contains(&self.needle) and the inverse:
if !input.contains(&self.needle) {
Err(format!("must contain '{}'", self.needle))
} else {
Ok(())
}
and:
if input.contains(&self.forbidden) {
Err(format!("must not contain '{}'", self.forbidden))
} else {
Ok(())
}
collect_errors, a plain for loop is the most readable:
let mut errors = Vec::new();
for v in validators {
if let Err(msg) = v.check(input) {
errors.push(msg);
}
}
errors
&dyn Validator means each v in the loop
is a &&dyn Validator. Method calls auto-deref, so v.check(input)
works without ceremony.validators.iter().filter_map(|v| v.check(input).err()).collect().You implemented an stdlib trait (Display), defined your own
(Describable), wrote a generic function with a trait bound, gave
a trait a default method that one type inherited and another
overrode (the Logger step), and finally swapped a generic for a
trait object so a single slice could hold a mix of validation rules.
What we learned
- A trait is a named collection of method signatures. Any type can opt in with
impl TraitName for TypeName { ... }. Same idea as Java/C# interfaces, Haskell type classes, Swift protocols, or C++ abstract classes with pure virtual methods.#[derive(...)]is sugar: the compiler writes the obviousimpl Trait for Typeblock for you.Debug,PartialEq,Eq,Clone,Copy,Default,Hash, andOrdare the everyday derivable ones.Displayis not derivable because there's no one-size-fits-all human-readable format.impl Displayis thetoString/__str__of Rust. Implementfn fmt(&self, f: &mut Formatter<'_>) -> fmt::Resultwith a singlewrite!(...)call.- Trait bounds on a generic say "I accept any
Tthat implements this trait."fn f<T: Trait>(x: &T)is the basic form,T: A + Bcombines bounds, andwhereclauses let you push long bounds out of the signature.- Default methods in the trait body give every implementor a baseline behavior. Override per type when you need to.
- Generics dispatch statically: one specialized copy of the function per concrete
T. Fast, but one call can only see one concrete type. Great default.- Trait objects (
dyn Trait) dispatch dynamically through a vtable. Slower per call but lets one slice orVechold values of multiple concrete types at once. Reach for them when you need heterogeneity.Box<dyn Trait>solves the "trait objects have no known size" problem so they can live in owning containers likeVec. That's the headline of Chapter 15 on smart pointers.- Many stdlib traits you already use (
Iterator,From,Into,PartialEq, ...) are just regular traits. Nothing magic. You can define your own or implement the standard ones for your own types.