Traits, generics and lifetimes
Defining and implementing traits, default methods, bounds and where clauses, static versus dynamic dispatch, lifetime elision, and practical borrow-checker rules.
Traits and default methods
trait Summary {
fn summarize(&self) -> String;
// a default method: implementors get it for free
fn preview(&self) -> String {
let s = self.summarize();
s.chars().take(60).collect()
}
}
struct Article {
title: String,
body: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {} chars", self.title, self.body.len())
}
}
// implement your trait for a foreign type, or a foreign trait for your type
impl Summary for Vec<Article> {
fn summarize(&self) -> String {
format!("{} articles", self.len())
}
}
fn main() {
let a = Article { title: "Rust".into(), body: "text".into() };
println!("{}", a.summarize());
println!("{}", a.preview()); // inherited default
}- A trait is a contract on behaviour. Any type can implement any trait as long as either the trait or the type is local to your crate — that is the orphan rule, and it keeps implementations unambiguous.
- Default methods can call the required methods, which is how a trait with one required method still offers a large derived surface.
Iteratoris the extreme case. - Associated types describe a trait's output, as in
Iterator::Item; a generic parameter describes an input choice. One implementation per type means an associated type; several possible implementations mean a generic parameter. Selfmeans the implementing type.impl Trait for &Tandimpl Trait for Box<T>are how library authors extend behaviour to references and smart pointers.- Derive what the compiler can write for you:
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]covers most data types.
Generics and dispatch
// one function, monomorphised per concrete type at compile time
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut max = list[0];
for &x in &list[1..] {
if x > max {
max = x;
}
}
max
}
// bounds can be written inline or in a where clause when they get long
fn join_all<T>(items: &[T], sep: &str) -> String
where
T: std::fmt::Display,
{
items.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(sep)
}
fn notify(item: &impl Summary) { // sugar for a generic parameter
println!("{}", item.summarize());
}
fn make_summary() -> impl Summary { // returns a concrete hidden type
Article { title: "hidden".into(), body: String::new() }
}
// dynamic dispatch: one code path, a vtable lookup per call
fn render_all(items: &[Box<dyn Summary>]) {
for i in items {
println!("{}", i.preview());
}
}
fn main() {
println!("{}", largest(&[3, 9, 4]));
println!("{}", largest(&["a", "c", "b"]));
}impl Trait / T: Trait | dyn Trait | |
|---|---|---|
| Dispatch | Static: resolved at compile time, inlined | Dynamic: vtable lookup at run time |
| Cost | Zero overhead, code size grows per type | One code path, a pointer indirection per call |
| Heterogeneous collections | Not possible with a single concrete type | The usual reason to reach for it: Vec<Box<dyn Trait>> |
| Trait requirements | Must be object-safe for dyn; generic methods and Self rules apply | Object-safe traits only |
| Use it for | Hot paths, library APIs, anything the compiler can see through | Plugin-style lists, callbacks stored in structs, cutting compile time |
Lifetimes that matter
// the returned reference lives as long as both inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// elision: one input reference means the output borrows from it
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// a struct holding a reference needs a lifetime parameter
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self, text: &str) -> &str {
println!("{text}");
self.part // elided: tied to &self
}
}
// 'static means "for the whole program", not "heap allocated"
fn greeting() -> &'static str {
"hello"
}
fn main() {
let a = String::from("short");
let b = String::from("considerably longer");
println!("{}", longest(&a, &b));
let e = Excerpt { part: first_word(&b) };
println!("{}", e.part);
}- A lifetime is a name for a region of code during which a borrow is valid. It never affects run time and generates no code; it is proof for the compiler.
- Annotation is rarely needed. Elision covers the three common cases: one input reference,
&self, and no reference output. Add a name only when the compiler says it cannot infer one. - Lifetime parameters on a struct mean the struct borrows: it cannot outlive the data it points at, which is exactly the guarantee you want and the reason such a struct cannot be stored in a long-lived registry.
- 'static is the lifetime of the whole program. String literals have it,
Box::leakproduces it, and a global registry requiring it is often a sign that ownership should be shared withArcinstead. - When two borrows conflict, shrink a scope rather than reaching for
RefCell. Moving a read into an inner block frequently resolves the error with no runtime cost at all.
💡
Three rules of thumb resolve most borrow checker fights: end a borrow before mutating by copying the value out or scoping the borrow; do not hold a
&mut across a call that needs another borrow of the same value; and when a function output borrows from an input, name that relationship in the signature rather than cloning to silence the error.FAQ
Should I use dyn Trait or generics?
Generics by default: static dispatch is faster and the compiler optimises through it. Use
dyn Trait when you need a collection of different types behind one interface, or when monomorphisation is inflating compile times and binary size.Why does adding a lifetime parameter fix a struct?
The struct stores a reference, so the compiler must know how long that reference is valid. The parameter states that the struct borrows from something and therefore cannot outlive it — without it there is no way to check the struct's own validity.
Related
Collections, strings and iterators Smart pointers and interior mutability
Last refreshed 2026-09-18.