Intermediate · Modules, crates, traits and generics · topic 29 of 50 · 30 min
Generics and trait bounds
Explanation
Why generics?
Imagine writing largest_i32, largest_f64 and largest_char: three functions with the same logic and different types. Generics remove this duplication. A generic is a placeholder for a type, written in angle brackets, usually named T (for "type"). You have already used generics: Vec<T>, Option<T>, Result<T, E> and HashMap<K, V>.
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut big = list[0];
for &item in list {
if item > big {
big = item;
}
}
big
}
fn main() {
println!("{}", largest(&[34, 50, 25, 100, 65])); // 100
println!("{}", largest(&[1.5, 0.2, 9.9])); // 9.9
println!("{}", largest(&['y', 'm', 'a', 'q'])); // y
}Trait bounds: what T must be able to do
Inside largest we compare with > and copy values out. Not every type can do that. A trait bound limits T to types that implement certain traits. T: PartialOrd + Copy means "T can be compared and copied". Without the bound, the compiler says "binary operation > cannot be applied to type T" and suggests the bound.
Three ways to write bounds
// 1. inline
fn show<T: std::fmt::Display>(x: T) { println!("{x}"); }
// 2. where clause: easier to read with many bounds
fn report<T, U>(a: T, b: U) -> String
where
T: std::fmt::Display + Clone,
U: std::fmt::Debug,
{
format!("{a} / {:?}", b)
}
// 3. impl Trait in argument position (short form of 1)
fn show2(x: impl std::fmt::Display) { println!("{x}"); }Generic structs and impl blocks
#[derive(Debug)]
struct Pair<T> {
a: T,
b: T,
}
impl<T: PartialOrd + std::fmt::Display> Pair<T> {
fn bigger(&self) -> &T {
if self.a >= self.b { &self.a } else { &self.b }
}
}
fn main() {
let p = Pair { a: 7, b: 12 };
println!("{}", p.bigger()); // 12
}impl Trait in return position
fn make_counter() -> impl Iterator<Item = u32> says "I return some type that is an iterator of u32; you do not need its exact name". This is very useful for iterators and closures, whose real types are long or impossible to write. The function must still return one single concrete type.
Monomorphization: zero-cost generics
When you call largest with i32 and with f64, the compiler generates two specialised copies, largest::<i32> and largest::<f64>, as if you had written them by hand. This is called monomorphization. The result runs as fast as hand-written code. The cost is compile time and a larger program file when a generic is used with many types.
largest(&[1, 2]) -> compiler makes fn largest_i32(list: &[i32]) -> i32
largest(&[1.0, 2.0]) -> compiler makes fn largest_f64(list: &[f64]) -> f64Turbofish
Sometimes the compiler cannot guess T. Then you name it with the "turbofish" syntax ::<>, which you met with parse::<u32>() and collect::<Vec<_>>().
Common mistakes
- Missing bounds: using > or {} on T without PartialOrd or Display. Add the bound the compiler suggests.
- Too many bounds "just in case". Ask only for what the function really uses.
- Returning different types from an impl Trait function (one branch a Vec iterator, another a range). It must be one concrete type; use Box<dyn Trait> for that case (next topic).
- Forgetting <T> after impl: write impl<T> Pair<T>, not impl Pair<T>.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Generics and trait bounds · 1/6
Generics let one piece of code work with many types. You have used them already: Vec, Option and Result.
Prefer a video by a teacher? Find free videos on “rust generics trait bounds where clause monomorphization” on YouTube ↗
In real life: A generic paginated API response
A shopping API returns pages of products, pages of orders and pages of reviews. Instead of three almost identical structs, the team writes one generic Page<T> with items, page number and total. Each endpoint returns Page<Product>, Page<Order> or Page<Review>, and the same helper methods work for all of them.
#[derive(Debug)]
struct Page<T> {
items: Vec<T>,
page: u32,
total_pages: u32,
}
impl<T> Page<T> {
fn has_next(&self) -> bool {
self.page < self.total_pages
}
}
fn main() {
let p = Page { items: vec!["kurta", "saree"], page: 1, total_pages: 3 };
println!("{} items, next page? {}", p.items.len(), p.has_next());
}Practice (0/3 done)
Why does this not compile? fn show<T>(x: T) { println!("{}", x); }
What does monomorphization mean?
Write a generic fn count_matching<T: PartialEq>(items: &[T], target: &T) -> usize. Use it with numbers and with &str values. Then write a generic struct Stack<T> with push, pop -> Option<T> and is_empty methods, and use it with u32 and String.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Trait objects and dynamic dispatchModules, crates, traits and generics · same module
- Packages, crates and modulesModules, crates, traits and generics · same module
- GenericsJava Programming (Core to Spring Boot) · Exceptions and generics
- Generics: type parameters and constraintsGo Programming (backend and cloud) · Packages, testing and generics
- Generics: reusable, type-safe codeC# and .NET · Collections, generics and LINQ
- Interview questions: traits, errors, pointers and concurrencyRust careers and interviews
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is T in fn f<T>(x: T)?
Answer: A type parameter (placeholder for a type)
T stands for any type that meets the bounds.
What does T: PartialOrd + Copy mean?
Answer: T must be both comparable and copyable
+ combines several required traits.
Are generics slower than hand-written code for each type?
Answer: No, monomorphization makes them as fast
Specialised copies are generated at compile time.
What does -> impl Iterator<Item = u32> promise?
Answer: The function returns some single concrete type that is an iterator of u32
impl Trait in return position hides one concrete type.
Which is the turbofish syntax?
Answer: parse::<u32>()
::<> names a generic type explicitly.
What is a trait?Related
Answer: A set of methods types can promise to implement
Traits describe shared behaviour.
What do generics mainly give you?Related
Answer: Type safety at compile time without casts
Errors move from run time to compile time.
From Generics
Why must dyn Trait be behind a pointer like Box?Related
Answer: Its size is not known at compile time
Different implementing types have different sizes.
What is Self inside impl Wallet?Related
Answer: A short name for the type Wallet
Self (capital S) refers to the type; self (small s) to the instance.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Modules, crates, traits and generics” before its exam: Trait objects and dynamic dispatch.
- Module 7 exam: Modules, crates, traits and generics · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Closures, iterators and smart pointers. 50 topics left in the course before the final exam.