Generics, Traits, and Lifetimes
Generics stand in for concrete types; traits name shared behavior; lifetimes name how long a borrow may live.
<!-- hal:authoritative:yaml -->
Generics stand in for concrete types; traits name shared behavior; lifetimes name how long a borrow may live. Together they delete duplication without deleting safety.
§I — Frame
Duha session 11. TRPL Chapter 10 is one syllabus row, so this fire takes the whole chapter at chapter pace. Session 10 left you propagating Result with ?. Keep that habit; today the types those functions talk about become placeholders you name.
You already used generics without writing them: Option<T>, Vec<T>, HashMap<K, V>, Result<T, E>. Chapter 10 teaches you to define your own. Three moves land by the end:
- Generic type parameters — write one
largestthat works for more thani32. - Traits and bounds — name shared behavior, then restrict
Tso the body is legal. - Named lifetimes — tell the compiler how input and output borrows relate so dangling references stay compile-time errors.
Syllabus Done-criteria: define a trait, implement it, and write a generic with a bound and a named lifetime.
§II — Extract, then generalize
Start where the book starts: duplication you can see. Finding the largest number in a list is a loop with a running reference. Call that loop twice on two lists and you have copied code. Extract a function that takes &[i32] and returns &i32. That removes value duplication.
Type duplication is next. A largest_i32 and a largest_char share a body and differ only in the signature. Name a type parameter:
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Read it: “largest is generic over some type T.” The angle brackets declare the name before you use it, the same way a value parameter must be declared before the body mentions it. Convention favors short UpperCamelCase names; T means type.
This version will not compile yet. The > comparison is not defined for every T. The compiler’s help points at PartialOrd. That is a trait bound — hold that thought for §III.
Structs and enums take the same angle-bracket form:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
Option<T> and Result<T, E> are the same pattern you already call. Methods on a generic type put the type parameters on the impl line: impl<T> Point<T>.
Generics are not a runtime tax. Rust monomorphizes: at compile time it generates concrete copies for the types you actually use. You pay in binary size when many concrete types appear; you do not pay a dynamic dispatch fee for ordinary generic calls.
§III — Traits name behavior
A trait is a set of method signatures other types agree to provide:
pub trait Summary {
fn summarize(&self) -> String;
}
Implement it with impl Trait for Type:
pub struct NewsArticle {
pub headline: String,
pub author: String,
pub location: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
Default method bodies live in the trait; an empty impl Summary for NewsArticle {} keeps the default. You may implement a trait on a type only when the trait or the type (or both) is local to your crate — the orphan rule. That keeps two crates from fighting over the same external trait on the same external type.
Accept any type that implements a trait with impl Trait sugar:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
The longer form is the trait bound:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
Multiple bounds use +. Crowded signatures move bounds into a where clause so the name, parameters, and return type stay readable.
Now finish largest. Restrict T to types that can be ordered:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
PartialOrd is the shared behavior the comparison needs. Without the bound, the generic is too open; with it, the body is legal and callers still share one definition.
§IV — Lifetimes name borrow relationships
Every reference has a lifetime — the scope for which it is valid. Most of the time the compiler infers it. Annotate when the relationship among borrows is ambiguous.
A dangling reference is the thing lifetimes exist to refuse:
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
x dies at the end of the inner block; r would outlive its referent. The borrow checker rejects that. Fix it by keeping the data alive as long as the reference.
Generic lifetime parameters look like type parameters. The book’s longest function returns one of two string slices. Without annotations it will not compile — the signature does not say whether the return borrow comes from x or y:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
'a is not a duration you set at runtime. It is a contract: the returned reference is valid as long as both inputs are. The actual lifetime at a call site is the overlap of the two arguments — the shorter one wins.
Structs that hold references need the same declaration:
struct ImportantExcerpt<'a> {
part: &'a str,
}
An ImportantExcerpt instance cannot outlive the string slice it borrows.
Lifetime elision covers common cases (one input lifetime, methods with &self, and so on). When elision leaves ambiguity, the compiler demands a named parameter instead of guessing. 'static means the data may live for the whole program (string literals do). Prefer fixing a dangling or mismatched borrow over sprinkling 'static because an error message suggested it.
§V — One complete proof
- Define a small trait (for example
Summarywithsummarize) andimplit on a local struct. - Write
fn largest<T: PartialOrd>(list: &[T]) -> &T(or an equivalent generic with a bound) and call it on ani32slice and acharslice. - Write
fn longest<'a>(x: &'a str, y: &'a str) -> &'a strand return the longer slice. - Say aloud: generics delete type duplication; traits constrain what those types can do; lifetimes constrain how long borrows may live.
When those four hold, Chapter 10’s selected depth is done.
§VI — Closing
Chapter 10 is the hinge between “I use Vec and Result” and “I author abstractions the type checker can still police.” Extract a function for values, then a type parameter for types, then a trait bound so the body stays honest, then a lifetime so returned references cannot outlive their sources. Monomorphization keeps the abstraction cheap. Session 09’s collections and session 10’s Result sit underneath; testing is the next unmarked row.
Done-criteria: define a trait, implement it, and write a generic with a bound and a named lifetime.