Day 4 · Concept 11
Generics
Type parameters with trait bounds. Rust monomorphises: each instantiation generates specialised code, zero runtime cost. The cost is in binary size and compile time — but the resulting code is as fast as if you'd hand-written each version.
1 · Try it — generic function
fn largest<T: PartialOrd>(items: &[T]) -> &T {
let mut biggest = &items[0];
for x in items {
if x > biggest {
biggest = x;
}
}
biggest
}
fn main() {
println!("{}", largest(&[1, 5, 3, 9, 2]));
println!("{}", largest(&["go", "rust", "c"]));
}2 · Multiple bounds, where clauses
use std::fmt::Debug;
use std::hash::Hash;
// Inline bounds — short
fn one<T: Clone + Debug>(x: T) { println!("{:?}", x.clone()); }
// where clause — longer signatures
fn two<T, U>(x: T, y: U) -> String
where
T: Debug + Hash,
U: Debug + Clone,
{
format!("{:?} {:?}", x, y.clone())
}3 · Generic types — structs and enums
struct Pair<T> {
first: T,
second: T,
}
impl<T: std::fmt::Display> Pair<T> {
fn print(&self) {
println!("{} {}", self.first, self.second);
}
}
fn main() {
let pi = Pair { first: 1, second: 2 };
pi.print();
let ps = Pair { first: "a", second: "b" };
ps.print();
}4 · Monomorphisation — what the compiler does
When you call largest::<i32>(...) and largest::<&str>(...),
the compiler generates two versions of largest — one specialised
for each type. Zero runtime overhead compared to writing them by hand.
The trade. Faster runtime; slower compile times; larger binaries.
For most projects, fine. For huge libraries, generic-heavy code compiles slowly.
5 · When it clicks
- You reach for generics when you'd duplicate a function for different types.
- Trait bounds describe what your function needs from
T, not whatTis. - You use
whereclauses for anything over 3 bounds.
Found this useful?