Rust Generics
debt(d2/e3/b4/t5)
Closest to 'caught instantly' (d1) but slightly worse at d2; missing trait bounds are compiler errors caught instantly, and clippy flags over-generification, though performance traps like monomorphization bloat stay silent until measured.
Closest to 'simple parameterised fix' (e3); the quick_fix is adding a trait bound (T: Display) or swapping Box<dyn Trait> for a generic bound, a small localized change but sometimes ripples through a function's signature and call sites.
Closest to 'localised tax' (b3) leaning to b4; generics applied across library/web/cli contexts shape public APIs, and heavy use creates a persistent productivity and compile-time/binary-size tax that touches multiple work streams.
Closest to 'notable trap' (t5); the misconception that Rust generics carry runtime overhead like reflection or boxing is a documented gotcha — in reality monomorphization is zero-cost — and devs commonly confuse generics with trait objects.
Also Known As
TL;DR
Explanation
Generics in Rust let you write functions, structs, enums, and methods that operate over any type rather than a single concrete one. You introduce type parameters with angle brackets, as in `fn largest<T>(list: &[T]) -> &T` or `struct Wrapper<T> { value: T }`. The same code then works for many types while the compiler still checks every usage statically. There is no boxing, no runtime type tags, and no reflection involved by default.
The defining mechanism is monomorphization. When you call a generic function with `i32` and `String`, the compiler generates a specialized copy for each concrete type at compile time, exactly as if you had written both versions by hand. This means generic code runs as fast as hand-specialized code - there is zero runtime dispatch overhead. The trade-off is larger binaries and longer compile times when many instantiations exist, which is the opposite trade-off from dynamic dispatch through `dyn`.
Generics rarely stand alone. To do anything useful with a value of type `T` you must constrain it with trait bounds: `fn print_all<T: Display>(items: &[T])` says T must implement Display. Without a bound the compiler only knows the type exists, not what operations it supports, so attempts to add, compare, or print it fail. Bounds can be written inline or in a `where` clause for readability, and you can require multiple traits with `T: Clone + Ord`.
Generics also power associated types, generic methods on impl blocks, and generic enums like `Option<T>` and `Result<T, E>` that underpin the entire standard library. A common confusion is mixing generics (static, monomorphized, one type chosen per call site) with trait objects (`Box<dyn Trait>`, dynamic, one type erased behind a vtable). Generics give you compile-time specialization and zero cost; trait objects give you runtime heterogeneity at the price of indirection.
Mastering generics means understanding type parameters, trait bounds as the gateway to actually using generic values, monomorphization and its code-size implications, and when a generic bound beats a trait object.
Common Misconception
Why It Matters
Common Mistakes
- Writing a generic function that uses an operation like comparison or printing without adding the required trait bound such as Ord or Display.
- Reaching for Box<dyn Trait> for runtime dispatch when a generic with a trait bound would be zero-cost and simpler.
- Over-generifying a function that only ever receives one concrete type, adding noise without any reuse benefit.
- Forgetting that heavy use of generics causes monomorphization bloat, inflating binary size and compile times.
- Assuming you can name a single generic instantiation at runtime, when each call site fixes the type at compile time.
Avoid When
- A function or struct will only ever work with a single concrete type, where a generic parameter adds complexity for no reuse.
- You need a heterogeneous collection of differing concrete types behind one interface, which calls for trait objects instead.
- Excessive instantiations would bloat the binary and compile times beyond what the performance gain justifies.
When To Use
- Writing functions or data structures that should operate over many types while preserving compile-time type safety.
- Achieving zero-cost abstraction in performance-critical code where dynamic dispatch overhead is unacceptable.
- Building reusable library APIs like containers, algorithms, and wrappers parameterized over caller-chosen types.
- Combining with trait bounds to express exactly what capabilities a generic type must provide.
Code Examples
// Generic without a bound: the compiler does not know T can be compared,
// so this fails to compile.
fn largest<T>(list: &[T]) -> &T {
let mut biggest = &list[0];
for item in list {
if item > biggest { // error: binary operation `>` cannot be applied to type `&T`
biggest = item;
}
}
biggest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("{}", largest(&numbers));
}
// Add a trait bound so T supports comparison; monomorphized per concrete type,
// zero runtime cost.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut biggest = &list[0];
for item in list {
if item > biggest {
biggest = item;
}
}
biggest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let chars = vec!['y', 'm', 'a', 'q'];
println!("{}", largest(&numbers));
println!("{}", largest(&chars));
}