Generics & Trait Bounds
Imagine you write a function that counts how many marks in a list are above a limit. Next week you need the same thing for heights (decimals), then for names in alphabetical order. Copying the function three times with different types would work, but every bug fix would have to be made three times.
Generics let you write the function once with a placeholder type, and trait bounds say what that type must be able to do. Rust then produces fast, type-specific code for each use. You have already used generic types: Vec<T>, Option<T> and Result<T, E>.
Generic functions
A generic function declares type parameters in angle brackets after its name. By convention they are single capital letters, usually T:
use std::fmt::Display;
fn count_above<T: PartialOrd>(items: &[T], limit: T) -> usize {
let mut count = 0;
for item in items {
if *item > limit {
count += 1;
}
}
count
}
fn show_all<T: Display>(label: &str, items: &[T]) {
print!("{label}:");
for item in items {
print!(" {item}");
}
println!();
}
fn main() {
let marks = [45, 82, 67, 91];
let heights = [1.62, 1.75, 1.58];
let names = ["Aarav", "Meera", "Kabir"];
println!("{}", count_above(&marks, 60));
println!("{}", count_above(&heights, 1.6));
println!("{}", count_above(&names, "Isha"));
show_all("Marks", &marks);
show_all("Heights", &heights);
}3 2 2 Marks: 45 82 67 91 Heights: 1.62 1.75 1.58
T: PartialOrd is a trait bound: "T can be any type, as long as it implements PartialOrd", which is the trait behind < and >. Without the bound, the compiler would reject *item > limit, because not every type can be compared. For text, > compares alphabetically, so "Meera" and "Kabir" come after "Isha".
The bound is a promise checked when you call the function. count_above(&[Some(1)], ...) works because Option<i32> implements PartialOrd, but passing a type that does not implement it is a compile error that names the missing trait.
Generic structs and impl blocks
Structs and enums can be generic too. The impl block must declare the type parameter again with impl<T>. You can also write an impl block that only applies when T meets a bound, so some methods exist only for some types:
use std::fmt::Display;
struct Shelf<T> {
items: Vec<T>,
capacity: usize,
}
impl<T> Shelf<T> {
fn new(capacity: usize) -> Self {
Self { items: Vec::new(), capacity }
}
fn add(&mut self, item: T) -> Result<(), T> {
if self.items.len() == self.capacity {
return Err(item); // shelf is full: give the item back
}
self.items.push(item);
Ok(())
}
}
impl<T: Display> Shelf<T> {
fn print(&self) {
for (i, item) in self.items.iter().enumerate() {
println!("{}: {item}", i + 1);
}
}
}
fn main() {
let mut books: Shelf<String> = Shelf::new(2);
books.add(String::from("Wings of Fire")).unwrap();
books.add(String::from("Malgudi Days")).unwrap();
if let Err(book) = books.add(String::from("Godaan")) {
println!("No space for {book}");
}
books.print();
let mut jars = Shelf::new(3);
jars.add(250).unwrap();
jars.add(500).unwrap();
jars.print();
}No space for Godaan 1: Wings of Fire 2: Malgudi Days 1: 250 2: 500
Shelf<String> and Shelf<i32> are two different types made from one definition. add returns Err(item) so that the caller gets the rejected item back instead of losing it. print lives in a separate impl<T: Display> block, so a Shelf of a type that cannot be displayed simply has no print method.
Several bounds and where clauses
Combine bounds with +. When the list gets long, move the bounds into a where clause after the signature, which keeps the first line readable:
use std::fmt::Debug;
fn best_entry<K, V>(entries: &[(K, V)]) -> Option<&(K, V)>
where
K: Debug,
V: PartialOrd,
{
let mut best: Option<&(K, V)> = None;
for entry in entries {
match best {
Some(current) if current.1 >= entry.1 => {}
_ => best = Some(entry),
}
}
best
}
fn main() {
let sales = [("Rohtak", 340), ("Jaipur", 515), ("Pune", 498)];
println!("{:?}", best_entry(&sales));
let empty: [(&str, u32); 0] = [];
println!("{:?}", best_entry(&empty));
}Some(("Jaipur", 515))
NoneThe function returns a reference into the slice, wrapped in an Option for the empty case. K: Debug is needed only so that the caller can print the result with {:?}.
impl Trait as a shortcut
In argument position, item: &impl Display is a shorter way to write a generic parameter with a bound. In return position, -> impl Trait means "this function returns some type that implements the trait, without naming it". This is how functions return iterators and closures, whose real types are long or cannot be written at all:
fn even_numbers(limit: u32) -> impl Iterator<Item = u32> {
(0..=limit).filter(|n| n % 2 == 0)
}
fn main() {
for n in even_numbers(8) {
print!("{n} ");
}
println!();
}0 2 4 6 8
You will learn about iterators and the |n| ... closure syntax in Iterators and Closures.
Const generics
Type parameters stand for types; const generics stand for constant values, most often array lengths. This lets one function accept arrays of any size while still knowing the size at compile time:
fn average<const N: usize>(marks: [u32; N]) -> f64 {
let mut total = 0;
for m in marks {
total += m;
}
total as f64 / N as f64
}
fn main() {
println!("{:.2}", average([70, 85, 90]));
println!("{:.2}", average([60, 75, 80, 95, 100]));
}81.67 82.00
Zero cost: monomorphization
Generics cost nothing at run time. During compilation Rust looks at every concrete type you used and generates a separate copy of the generic code for it, a process called monomorphization. count_above above becomes three functions: one for i32, one for f64 and one for &str, each as fast as if you had written it by hand. The trade-off is longer compile times and a larger program when a generic is used with many types. When you sometimes need to name the type explicitly, use the turbofish: "42".parse::<u8>() or Vec::<u32>::with_capacity(10).
Common mistakes
- Forgetting a bound. "binary operation
>cannot be applied to typeT" means you needT: PartialOrd. Add the trait the compiler names. - Forgetting
<T>afterimpl. Writeimpl<T> Shelf<T>, notimpl Shelf<T>. - Over-generalising. A function used with one type does not need to be generic. Add generics when you have a second real use.
- Expecting
impl Traitto return different types. Every return path of-> impl Traitmust return the same concrete type; useBox<dyn Trait>when it varies.
Key points
- Generics (
fn f<T>,struct S<T>) let one definition work for many types. - Trait bounds (
T: PartialOrd + Display, or awhereclause) say what a type must support. impl<T: Bound> Type<T>blocks add methods only for types that meet the bound.impl Traitis shorthand in arguments and hides complex return types such as iterators.- Const generics (
const N: usize) make array lengths generic, and monomorphization makes all generics zero-cost at run time.
Exercise
Write a generic function fn most_common<T: PartialEq + Copy>(items: &[T]) -> Option<T> that returns the value that appears most often (the first one in case of a tie), or None for an empty slice. Use two nested loops. Test it with the runs scored per ball [1, 4, 1, 0, 6, 1, 4] and with the characters of a word.
Show solution
fn most_common<T: PartialEq + Copy>(items: &[T]) -> Option<T> {
let mut best: Option<(T, usize)> = None;
for a in items {
let mut count = 0;
for b in items {
if a == b {
count += 1;
}
}
match best {
Some((_, best_count)) if best_count >= count => {}
_ => best = Some((*a, count)),
}
}
best.map(|(value, _)| value)
}
fn main() {
println!("{:?}", most_common(&[1, 4, 1, 0, 6, 1, 4]));
let letters: Vec<char> = "kanpur".chars().collect();
println!("{:?}", most_common(&letters));
let empty: [u8; 0] = [];
println!("{:?}", most_common(&empty));
}Some(1)
Some('k')
NonePartialEq allows == and Copy allows returning a copy of the value with *a. In "kanpur" every letter appears once, so the first one, 'k', wins the tie. The final map drops the count and keeps only the value.
Quick Quiz
1. What does the bound in fn f<T: Display>(x: T) mean?
A trait bound restricts the generic type to types that implement the named trait.
2. How does Rust make generic code as fast as non-generic code?
Monomorphization creates type-specific code at compile time, so there is no run-time cost.
3. Which line correctly starts an impl block for a generic struct Shelf<T>?
The type parameter must be declared after impl before it can be used in Shelf<T>.
4. What does fn sum<const N: usize>(a: [u32; N]) accept?
N is a const generic parameter, inferred from the array passed in.
Learn Rust with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
