Generics
Write functions and structs that work over any type satisfying a trait bound — zero-cost abstraction through monomorphization.
- Write generic functions with type parameters
- Add trait bounds using both inline and where clause syntax
- Define generic structs and implement methods on them
- Explain what monomorphization means and why it has no runtime cost
Generics let you write a single function or data structure that works over many concrete types. You've been using generics since the beginning: Vec<T>, Option<T>, Result<T, E> are all generic types. This lesson covers how to write your own.
The key property that makes Rust generics compelling is zero cost: the compiler generates specialised code for every concrete type used, so there is no runtime overhead from abstraction. This is different from Java generics (which use type erasure) or Python (which is dynamic).
Generic functions
A generic function declares one or more type parameters in angle brackets:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut biggest = &list[0];
for item in list {
if item > biggest {
biggest = item;
}
}
biggest
}T: PartialOrd is a trait bound — it constrains T to types that support the > operator. Without it, the compiler doesn't know if T values can be compared.
let numbers = vec![34, 50, 25, 100, 65];
println!("{}", largest(&numbers)); // 100
let chars = vec!['y', 'm', 'a', 'q'];
println!("{}", largest(&chars)); // yThe same function works for i32 slices and char slices because both implement PartialOrd.
Generic structs
Structs can also be generic:
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Self { first, second }
}
}
impl<T: PartialOrd + std::fmt::Display> Pair<T> {
fn display_max(&self) {
if self.first > self.second {
println!("Largest: {}", self.first);
} else {
println!("Largest: {}", self.second);
}
}
}Note two separate impl blocks: the first works for any T; the second only compiles for types that implement both PartialOrd and Display. You can add methods conditionally based on what bounds T satisfies.
The compiler generates separate code for Pair<i32> and Pair<String> (and any other concrete T). This process — monomorphization — means calling Pair<i32>::display_max() is as fast as a function written specifically for i32. The abstraction has no runtime cost.
where clauses
When trait bounds get long, where clauses improve readability:
// Hard to read:
fn compare_and_display<T: PartialOrd + std::fmt::Display, U: std::fmt::Display>(t: T, u: U) {
// ...
}
// Easier to read:
fn compare_and_display<T, U>(t: T, u: U)
where
T: PartialOrd + std::fmt::Display,
U: std::fmt::Display,
{
// ...
}Both are equivalent. The where form scales better when you have many bounds or complex relationships between parameters.
Multiple bounds
Use + to require multiple traits from the same type parameter:
fn print_largest<T: PartialOrd + std::fmt::Display>(list: &[T]) {
let max = list.iter().max_by(|a, b| a.partial_cmp(b).unwrap());
if let Some(m) = max {
println!("Largest: {}", m);
}
}T: PartialOrd + Display means "T must support both comparison and formatting."
Generics vs impl Trait
The impl Trait syntax from the traits lesson and explicit generics are related but not identical:
// impl Trait — opaque, different concrete types allowed per call, can't name the type
fn notify(item: &impl Summary) { ... }
// Generic — the type is named, can be reused across parameters
fn notify_pair<T: Summary>(item1: &T, item2: &T) { ... }In the generic version, item1 and item2 must be the same concrete type. With impl Trait, they could be different types. Use generics when you need to express relationships between parameter types.
If the compiler says "type annotations needed," you likely need to help it infer the type parameter. Either annotate the variable type (let v: Vec<i32> = Vec::new()) or use the turbofish syntax (Vec::<i32>::new()). The turbofish is rare in practice — usually an annotated let is cleaner.
Check your understanding
Knowledge check
- 1.Why does a generic function that uses > to compare two T values need a T: PartialOrd bound?
- 2.What is monomorphization in the context of Rust generics?
- 3.A where clause is semantically different from an inline trait bound — it enables features not available with inline syntax.
Do it yourself
Write a generic Stack<T> struct backed by a Vec<T>, with methods push(&mut self, val: T), pop(&mut self) -> Option<T>, and peek(&self) -> Option<&T>. Then add a display_top method constrained to T: Display that prints the top element without popping it. Observe that Stack<i32> gets display_top but Stack<Vec<u8>> does not.
Where to go next
Generics with trait bounds are how Rust achieves zero-cost abstraction for data structures and functions. The next lesson on closures and iterators applies these ideas to one of Rust's most expressive features.