Generics let one piece of code work for many types with no runtime overhead, and you read them in every API signature.
Generics let you write code once for many types. fn largest<T: PartialOrd>(items: &[T]) -> &T works for slices of integers, floats or strings. Structs and enums can be generic too: struct Pair<T> { a: T, b: T }, and of course Vec<T>, Option<T> and Result<T, E>.
Inside a generic function you can only do what the trait bounds promise. Without T: PartialOrd you cannot compare with >; without T: Clone you cannot call .clone(). The compiler checks the generic body once against the bounds, so errors appear at the definition, not deep inside an instantiation.
Bounds can be written inline (T: Display + Clone) or in a `where` clause, which reads better when there are several:
fn report<K, V>(map: &HashMap<K, V>) where K: Display + Eq + Hash, V: Debug { }impl blocks can be generic (impl<T> Pair<T> { }) or conditional: impl<T: Display> Pair<T> { fn show(&self) } adds methods only when T qualifies. A blanket impl like impl<T: Display> ToString for T gives a trait to every type meeting a bound.
Rust compiles generics by monomorphization: it generates a specialised copy for each concrete type used. That makes them as fast as hand-written code, at the cost of compile time and binary size. When that matters, or when you need a collection of mixed types, trait objects (dyn Trait, a later lesson) trade a little runtime speed for one shared copy.
impl Trait in argument position is shorthand for an anonymous generic parameter. Use a named T when you need to mention the type twice or let callers specify it with turbofish syntax: parse::<u32>(), collect::<Vec<_>>().
Generics also accept const parameters: fn first<T, const N: usize>(arr: [T; N]).
Example
use std::fmt::Display;
fn largest<T: PartialOrd>(items: &[T]) -> &T {
let mut best = &items[0];
for item in items {
if item > best { best = item; }
}
best
}
struct Pair<T> { a: T, b: T }
impl<T> Pair<T> where T: Display + PartialOrd { // methods only for comparable, printable T
fn show_max(&self) {
let m = if self.a > self.b { &self.a } else { &self.b };
println!("max = {m}");
}
}
fn sum_all<const N: usize>(xs: [i32; N]) -> i32 { xs.iter().sum() }
fn main() {
println!("{} {}", largest(&[3, 9, 2]), largest(&["kiwi", "apple"])); // 9 kiwi
Pair { a: 1.5, b: 0.5 }.show_max(); // max = 1.5
let v = "1 2 3".split(' ').map(|s| s.parse::<i32>().unwrap()).collect::<Vec<_>>();
println!("{v:?} {}", sum_all([1, 2, 3, 4])); // [1, 2, 3] 10
}Remember
- Trait bounds define what a generic body is allowed to do with
T - Use
whereclauses for long bounds; conditionalimplblocks add methods per bound - Generics are monomorphized: zero runtime cost, more compile time and code size
Watch out for
- Adding bounds to a struct definition instead of only to the impl blocks that need them.