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. Iterator is 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.
  • Self means the implementing type. impl Trait for &T and impl 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: Traitdyn Trait
DispatchStatic: resolved at compile time, inlinedDynamic: vtable lookup at run time
CostZero overhead, code size grows per typeOne code path, a pointer indirection per call
Heterogeneous collectionsNot possible with a single concrete typeThe usual reason to reach for it: Vec<Box<dyn Trait>>
Trait requirementsMust be object-safe for dyn; generic methods and Self rules applyObject-safe traits only
Use it forHot paths, library APIs, anything the compiler can see throughPlugin-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::leak produces it, and a global registry requiring it is often a sign that ownership should be shared with Arc instead.
  • 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.

Collections, strings and iterators Smart pointers and interior mutability

Last refreshed 2026-09-18.