Generics, Traits, and Lifetimes
Rust's three pillars of abstraction — parameterized types, shared behavior, and reference validity proofs.
Part of Rust Interview Mastery · Based on The Rust Programming Language
Generics
Explain generics in Rust and what monomorphization means.
Generics parameterize code over types: fn largest<T: PartialOrd>(list: &[T]) -> &T. At compile time the compiler performs monomorphization — it generates a concrete largest_i32, largest_f64, etc. for every T actually used.
The payoff interviews care about: zero-cost abstraction. Generic code is as fast as hand-written per-type code — no boxing, no vtable, no runtime dispatch. Cost: bigger binary and longer compile times. (Option<T>/Vec<T> are generics too — the type system is doing the same thing everywhere.)
Where can T appear? Walk through generic structs, enums, and impl blocks.
struct Point<T> { x: T, y: T }
impl<T> Point<T> { // impl<T> declares T for the block
fn x(&self) -> &T { &self.x }
}
impl Point<f32> { // concrete impl: only f32 Points get this
fn dist(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() }
}
The impl<T> after impl is required — it declares T so Point<T> knows it's generic. You can also give concrete-type impls (Point<f32>) that add methods only for specific instantiations. And like Point<T, U> with two params: struct Pair<T, U> { x: T, y: U }.
Are Rust generics like Java generics or C++ templates?
Closer to C++ in effect, different in safety. Like templates, monomorphization generates concrete code — no type erasure. Unlike templates, Rust type-checks the generic body once against declared bounds: T: PartialOrd guarantees > exists for every valid T.
Java uses type erasure — one bytecode version, casts at runtime, and generics can't touch primitives. Rust generics produce specialized machine code, work on primitives, and fail at declaration-site if bounds are wrong rather than at call-site with arcane substitution errors (C++'s famous failure mode).
Traits
What is a trait and how do you implement one?
trait Summary {
fn summarize(&self) -> String;
fn summarize_author(&self) -> String;
}
struct Tweet { username: String, content: String }
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
fn summarize_author(&self) -> String { format!("@{}", self.username) }
}
A trait declares shared behavior — method signatures (with optional bodies) — and any type can implement it. It's the Rust answer to interfaces. The type owns the impl; multiple types impl the same trait, one type impls many traits.
What are default trait implementations and why are they powerful?
A trait method can ship a body that uses the trait's other methods:
trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
Implementors get summarize free by only writing summarize_author — or override it. It's interface inheritance of behavior without class inheritance: shared default logic, no base-class fragility.
State the orphan rule — what can you implement on what?
You may impl Trait for Type only when the trait or the type is local to your crate. So: your trait on Vec — allowed; Display on your struct — allowed; Display on Vec (both foreign) — rejected.
Without the rule, two dependencies could write conflicting impls of the same trait for the same type — coherence would break. Workaround: the newtype pattern — wrap the foreign type (struct Wrapper(Vec<String>)) and impl on your wrapper.
Explain trait bounds and the where clause — including the sugar forms.
fn notify(item: &impl Summary) // sugar: one bounded param
fn notify<T: Summary>(item: &T) // equivalent generic form
fn pair<T: Summary + Display>(a: &T, b: &T) // multiple bounds
fn complex<T, U>(t: &T, u: &U)
where
T: Display + Clone, // readable form
U: Clone + Debug,
{ ... }
impl Trait in arg position = "some type implementing it." Generics+bounds = same, plus the bound applies to all uses of T (so a and b are the same concrete type — impl Trait in two params would allow different types!). where keeps long bound lists readable.
What does returning impl Trait mean, and what's its gotcha?
fn make() -> impl Summary promises "some concrete type implementing Summary" without naming it — callers use only the trait API. It's how Iterator chains stay ergonomic (-> impl Iterator<Item = T> hides a monster nested type).
The gotcha: it must return one concrete type. if c { Tweet } else { Article } behind impl Summary fails — two different types. For true runtime polymorphism you need Box<dyn Summary> (trait objects, ch 18).
What are blanket implementations? Give the std example.
An impl over a bound instead of a concrete type:
impl<T: Display> ToString for T { /* ... */ }
Every type that implements Display gets ToString for free — which is why "hi".to_string(), 5.to_string() etc. work everywhere. Blanket impls are how std gives huge API surface from few trait definitions: From/Into, IntoIterator, TryInto all cascade this way.
Lifetimes
What problem do lifetime annotations actually solve?
A reference must never outlive its referent — that's the dangling-pointer rule. The compiler can prove it within a function body, but across function signatures it can't guess intent. fn longest(x: &str, y: &str) -> &str — does the return borrow from x or y? Caller doesn't know; callee doesn't know.
Annotations are the contract: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str says "the return lives as long as the shorter of the two inputs." The compiler checks both sides against that contract. No runtime cost — it's purely type-system bookkeeping.
Read this signature aloud for an interview: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str.
"Both inputs and the return share lifetime 'a — the returned reference is valid for the intersection of the inputs' lifetimes; concretely, it can't outlive whichever input dies first."
The important nuance: 'a doesn't force both to live equally long — it names the common lifetime, which is the smaller of the two. If you drop x, the returned ref must already be dead. That's why longest(&s1, &s2) result can't escape the scope of the shorter-lived string.
What are the three lifetime elision rules?
If lifetimes aren't written, the compiler infers them:
- Each input reference gets its own lifetime —
fn f(x: &i32, y: &i32)→<'a,'b>. - If exactly one input lifetime, it's assigned to all outputs —
fn f(x: &i32) -> &i32→x's. - If there's
&self/&mut self, output lifetimes get self's — methods return refs into the object.
Elision covers common cases so fn first(s: &str) -> &str needs no annotation. When rules can't decide — two inputs, one output, no self — the compiler asks you to be explicit. That's the longest case.
When does a struct need a lifetime parameter?
Whenever it stores references: struct Excerpt<'a> { part: &'a str } means "an Excerpt cannot outlive the string it slices." Every method that returns the field propagates 'a.
It's the compiler tracking a relational fact: the struct borrows, so its max lifespan is bound to the referent's. This is why the default advice is "store owned data (String) unless measured otherwise" — owned fields need no lifetimes; borrowed fields infect every signature that touches them.
What is 'static, and when is it (mis)used?
'static = lives for the whole program. String literals (&'static str) live in the binary; leaked Box::leak memory qualifies; global statics are 'static by definition.
Misuse: slapping + 'static on a bound to silence a compiler error. Usually the real fix is restructuring ownership (return String, clone, Arc) — 'static bounds mean "owns nothing borrowed," and fighting toward them when data should be scoped creates clones and leaks. Legit uses: thread::spawn closures (must own/be 'static — ch 16), type-erased trait objects, error types.
How do generics, trait bounds, and lifetimes compose? Read this: fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str where T: Display.
Decode it piece by piece:
- <'a, T> — one lifetime param, one type param
- x, y — two &str inputs bound to 'a; return also 'a (shorter-input rule)
- ann: T — any type at all, zero lifetime constraints (it's owned, moved in)
- where T: Display — but it must be printable
The function can announce (println!("{ann}")) while returning a borrowed slice. The signature encodes: return borrows from the strings, announcement is a separate unconstrained generic. That's the three systems composing in one line — this is the book's capstone example for the chapter.