Traits and generics
Describing behaviour instead of types — a trait is a promise a type can keep — and writing one function that works for every type that keeps it.
A trait is a list of promises
A trait describes what a type can do, without saying what the type is: trait Summary { fn short(&self) -> String; }. Any type can then keep that promise in its own way — impl Summary for Book { … } — and code written against the trait works with all of them.
You have been using traits since the first chapter. println!("{:?}") needs the Debug trait, .to_string() needs Display, and #[derive(Clone, Debug)] above a struct is the compiler writing those implementations for you.
Generics with a bound
A function can be written for every type that has a trait: fn loudest<T: Summary>(items: &[T]) -> Option<&T>. Read the bound as a condition — for any T, as long as T is a Summary, this function exists. The compiler then checks every call: a type that has not kept the promise is a compile error, at the call site.
T is not a box of anything. Rust compiles a version per concrete type it is used with, so generic code runs as fast as the hand-written kind — and the bound is also documentation: the function only uses what it promised to use.
Where traits earn their place
A trait is the seam between the parts of a program: storage that could be a file or a database, a shape that could be a circle or a square. Writing the behaviour first keeps the parts apart, and impl Trait in a return type — -> impl Summary — says "something that promises this, and you do not need to know what".
The standard library is built this way. Once you know that Iterator, Display, Copy and Ord are traits, the methods that appear on your values stop being magic and become a list of promises somebody else already kept.
One trait, two types, one function
trait Summary {
fn short(&self) -> String;
}
struct Book { title: String, pages: u32 }
struct Film { name: String, minutes: u32 }
impl Summary for Book {
fn short(&self) -> String {
format!("{} ({} pages)", self.title, self.pages)
}
}
impl Summary for Film {
fn short(&self) -> String {
format!("{} ({} min)", self.name, self.minutes)
}
}
fn print_all<T: Summary>(items: &[T]) {
for item in items {
println!("- {}", item.short());
}
}
fn main() {
let books = vec![Book { title: String::from("The Rust Book"), pages: 420 }];
let films = vec![Film { name: String::from("Arrival"), minutes: 116 }];
print_all(&books);
print_all(&films);
}One function, two shapes
Run it, then add a third type of your own — a podcast, say, with a name and an episode count — implement Summary for it, and pass a list of them to the same print_all.
A trait with one method is a small promise and often the most useful kind. If a trait grows six methods before anything implements it, the seam is probably in the wrong place.
Default methods are a shortcut, not a requirement
A trait can provide a body for a method, and an implementor inherits it unless it writes its own. Put the method every type can agree on in the default, and leave only the one or two that really differ as required. Required methods say what the trait is about; default methods say what follows once you know that.
A default body can only use the required methods and the trait's own names. It cannot reach into a field of a type it has never seen — which is the point. It is written against the promise, not against a particular struct.
Bounds are conditions, and they stack
A generic function with T: Summary + Clone works for types that keep both promises; a where clause spells the same list out when it gets long. Bounds are checked at the call site, so a caller who passes the wrong type gets the error in their own code, not inside the library.
Rust compiles a separate copy of a generic function for each concrete type, so there is no run-time lookup and no boxing. A bound in an argument position is shorthand for a type parameter; in a return type it means "one concrete type that keeps this promise". Returning two different types from two branches does not work, even when both implement the trait, because the return type still has to be one thing.
The alternative to a type parameter is a trait object: &dyn Summary or Box<dyn Summary> holds any value that keeps the promise, with the concrete type erased. That allows a vector of mixed types — Vec<Box<dyn Summary>> — at the cost of a pointer indirection and a lookup when the method is called. Generics are faster and keep the type visible; dyn is the choice when the set of types is not known until the program runs. Reach for it when you need the flexibility, and for a generic when you do not.
Three trait mistakes. Implementing a trait you do not own for a type you do not own is blocked by the orphan rule; wrap the type in a newtype and implement the trait on the wrapper. A trait method is only callable when the trait is in scope, so add the use even for a type you already have. And a generic function may only use what its bounds promise — calling .clone() needs T: Clone as well, however cloneable the type looks.
Try these next
- Add a
defaultmethod to the trait that returns a fixed sentence, and let one type override it. - Write a function taking two bounds,
T: Summary + Clone, and call.clone()inside it. - Try to return a
Bookfrom one branch and aFilmfrom another under-> impl Summary, and read the compiler's explanation.
A trait with a default, used by two types
Run it, then add a `Book` type with an author field and implement the trait for it. Override `label` for `Book` so the author appears in the catalogue line.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does a trait describe?
