Generics
Generics let you write code that works with many types instead of just one, without giving up type safety or runtime performance. Instead of writing a separate largest_i32, largest_f64, and largest_char function, you write a single largest<T> function once, and the compiler generates the specialized versions for you. This is one of Rust’s core tools for eliminating duplication while keeping every guarantee the borrow checker and type system provide.
Overview: How Generics Work
A generic is a placeholder for a type that gets filled in later. When you write fn largest<T>(list: &[T]) -> T, T is not a real type — it is a stand-in that means "some type, to be determined by whoever calls this function." The angle brackets <T> declare that this function is generic over a type parameter named T. Inside the function body, you can use T anywhere you’d use a concrete type: as a parameter type, a return type, or a field type.
The crucial question is: what can you actually do with a value of type T inside the function? By default, almost nothing — the compiler has no idea what T will end up being, so it refuses to let you compare two T values with >, print one with {}, or copy one, because not every possible type supports those operations. This is where trait bounds come in: writing T: PartialOrd tells the compiler "whatever T turns out to be, it must implement the PartialOrd trait," which unlocks the > and < operators for that type inside the function. Bounds are a contract — the compiler checks the body of your generic function against exactly the capabilities the bounds promise, nothing more.
Under the hood, Rust generics are not like Java generics (which erase type information at runtime) or a dynamically-typed language’s duck typing. Rust uses monomorphization: at compile time, for every distinct concrete type your generic code is actually called with, the compiler generates a separate, fully specialized copy of that function or struct, as if you had hand-written it for that one type. Calling largest(&some_vec_of_i32) and largest(&some_vec_of_char) causes the compiler to emit two independent functions, largest_i32 and largest_char in spirit (the real names are mangled), each compiled and optimized exactly like non-generic code. The generic function itself never exists in the compiled binary — only its concrete instantiations do. This is why Rust generics are described as "zero-cost abstractions": there is no runtime dispatch, no boxing, and no performance penalty compared to writing the type-specific code by hand. The tradeoff is longer compile times and larger binaries when a generic is instantiated with many different types, since each instantiation duplicates the machine code.
Generics apply to more than functions. Structs, enums, and methods (via impl blocks) can all be generic over one or more type parameters, and you can mix generics with the ownership and borrowing rules you already know — a generic struct still has exactly one owner for each field, and a generic function’s borrow checking works exactly the same way it would for a concrete type.
Syntax
The general shapes you’ll use for generic items look like this:
fn function_name<T: Trait1 + Trait2>(param: T) -> T {
// body uses only what Trait1 and Trait2 guarantee
}
struct StructName<T> {
field: T,
}
impl<T: Trait> StructName<T> {
// methods
}
enum EnumName<T> {
Variant1(T),
Variant2,
}
fn generic_where<T>(param: T) -> T
where
T: Trait1 + Trait2,
{
// equivalent to the inline bound above, easier to read
// when there are several type parameters and bounds
}
| Piece | Meaning |
|---|---|
<T> |
Declares a type parameter named T. Any identifier works, but single uppercase letters (T, U, K, V) are the convention. |
T: Trait1 + Trait2 |
A trait bound: restricts T to types implementing both traits, and grants the function body those traits’ methods/operators. |
where T: Trait |
An alternate, more readable place to write bounds, especially with multiple parameters or complex bounds. |
impl<T> StructName<T> |
Implements methods for every possible T; the <T> after impl must be declared before it’s used after the struct name. |
StructName<T, U> |
Multiple independent type parameters — the fields can hold different concrete types. |
Examples
Example 1: A Generic Function
This function finds the largest item in a slice of any type that can be compared and copied:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("The largest number is {}", result);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
println!("The largest char is {}", result);
}
Output:
The largest number is 100
The largest char is y
The bound T: PartialOrd + Copy is doing two jobs. PartialOrd unlocks the > comparison inside the loop; Copy allows list[0] to be copied out of the borrowed slice into largest instead of trying to move a value out of something you only borrowed (which the borrow checker would reject). Because i32 and char both implement PartialOrd and Copy, the same function body works for both without any code duplication — the compiler generates one specialized version per call site’s type.
Example 2: A Generic Struct
struct Point<T> {
x: T,
y: T,
}
impl<T: std::fmt::Display> Point<T> {
fn describe(&self) -> String {
format!("({}, {})", self.x, self.y)
}
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.5, y: 2.5 };
println!("Integer point: {}", integer_point.describe());
println!("Float point: {}", float_point.describe());
}
Output:
Integer point: (5, 10)
Float point: (1.5, 2.5)
Point<T> is one struct definition that can hold an i32 pair, an f64 pair, or a pair of any other type — but within a single instance, x and y must be the same concrete type, because both fields share the one type parameter T. The impl<T: std::fmt::Display> block only provides describe for types that implement Display; a hypothetical Point holding a type without Display would simply not have a describe method available.
Example 3: A Generic Stack
A more realistic use of generics: a reusable last-in-first-out container.
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
fn main() {
let mut stack: Stack<String> = Stack::new();
stack.push(String::from("first"));
stack.push(String::from("second"));
stack.push(String::from("third"));
while !stack.is_empty() {
match stack.pop() {
Some(value) => println!("Popped: {}", value),
None => println!("Stack is empty"),
}
}
}
Output:
Popped: third
Popped: second
Popped: first
Stack<T> has no trait bounds at all — push, pop, and is_empty only move values in and out of a Vec<T>, which every type can do, so no bound is required. Notice pop returns Option<T>, not T directly: popping an empty stack is a normal, expected case in Rust, not an error, so the possibility of "nothing there" is encoded in the type itself rather than risking a runtime panic or a null value.
How It Works Step by Step: Monomorphization
Walk through what the compiler does with Example 1’s two calls to largest:
- The compiler sees
largest(&numbers)wherenumbers: Vec<i32>, so it infersT = i32for this call site. - It checks that
i32actually implements the boundsPartialOrdandCopy— it does, so the call is allowed. - The compiler generates a concrete version of
largestwith everyTreplaced byi32, as if you had writtenfn largest_i32(list: &[i32]) -> i32by hand. - It sees
largest(&chars)wherechars: Vec<char>, infersT = char, checks the same bounds againstchar, and generates a second, independent concrete version specialized forchar. - Each concrete version is compiled and optimized on its own, exactly like ordinary non-generic code — there is no shared "generic" machine code and no runtime type-checking or dispatch.
The practical upshot: a generic function call has identical performance to a hand-written, type-specific function. The cost is paid entirely at compile time (more code to generate and optimize), never at runtime.
Common Mistakes
Mistake 1: Missing Trait Bounds
Writing a generic function that uses an operator or method without declaring the trait that provides it fails to compile, because the compiler cannot assume any type supports it:
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
This fails with errors like "binary operation > cannot be applied to type T" and "cannot move out of index of [T]," because without bounds, T could be any type at all — including one that has no ordering and cannot be copied. The fix is to add the bounds the body actually needs, as shown in Example 1:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
Mistake 2: Assuming One Type Parameter Covers Different Types
A single type parameter used for two fields forces both fields to be the same concrete type in any given instance:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
The compiler infers T = i32 from x: 5, then rejects y: 4.0 with a "mismatched types" error, because 4.0 is an f64, not an i32. This surprises newcomers who expect generics to mean "any type, independently, per field." If the fields genuinely need to differ, declare a second type parameter:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 4.0 };
println!("x = {}, y = {}", mixed.x, mixed.y);
}
Now x is inferred as i32 and y as f64 independently, because they’re tied to separate type parameters.
Best Practices
- Only add the trait bounds a generic function’s body actually requires — extra bounds needlessly restrict which types can call it.
- Prefer
whereclauses over long inline bound lists once you have more than one type parameter or more than one or two bounds, for readability. - Use
&Tparameters instead of requiringT: Copywhen your function only needs to read the value, so it also works with non-Copytypes likeStringorVec<T>. - Remember generics are resolved at compile time: you cannot store a
Vecof "anyT" at runtime without a different mechanism (like trait objects withdyn Trait), since a generic collection is monomorphized to one concrete type per instantiation. - Reach for a second type parameter (
<T, U>) whenever two fields or parameters are conceptually unrelated types, rather than forcing an artificial shared type. - Keep an eye on compile times and binary size in large codebases with heavily generic APIs — every distinct instantiation duplicates code.
Practice Exercises
- Write a generic function
smallest<T: PartialOrd + Copy>(list: &[T]) -> Tthat returns the smallest item in a slice, and call it with both aVec<i32>and aVec<f64>. - Define a generic struct
Pair<T>with two fieldsfirst: Tandsecond: T, and write a methodswap(&mut self)that swaps the two fields in place. (Hint:std::mem::swapcan swap two mutable references.) - Extend the
Stack<T>from Example 3 with a methodpeek(&self) -> Option<&T>that returns a reference to the top item without removing it. Expected output when peeking a stack containing"only":Some("only")when printed with{:?}.
Summary
- Generics let one function, struct, enum, or method definition work across many concrete types.
- Type parameters like
Tare placeholders with no capabilities by default; trait bounds (T: Trait) grant the operations and methods the body needs. - Rust compiles generics via monomorphization: a separate, fully concrete copy is generated for each type actually used, giving zero runtime cost compared to hand-written type-specific code.
- A struct with one shared type parameter forces all fields using it to be the same concrete type in a given instance; use multiple type parameters when fields should vary independently.
- Generic code still obeys every ownership and borrowing rule — bounds like
Copyoften exist specifically to satisfy those rules inside the generic body. - Only require the bounds you actually use, and prefer
whereclauses for readability as bounds grow.
