In the previous tutorial, we learned traits — how to define shared behavior. Now we learn generics — how to write code that works with many types without repeating yourself.
You already use generics every day in Rust. Vec<T>, Option<T>, Result<T, E> — these are all generic types. The T is a placeholder for any type. In this tutorial, you learn to write your own.
Why Generics?
Without generics, you would write separate functions for each type:
fn largest_i32(list: &[i32]) -> &i32 { /* ... */ }
fn largest_f64(list: &[f64]) -> &f64 { /* ... */ }
fn largest_str(list: &[&str]) -> &&str { /* ... */ }
That is three functions doing the same thing. With generics, you write one:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut biggest = &list[0];
for item in &list[1..] {
if item > biggest {
biggest = item;
}
}
biggest
}
The <T: PartialOrd> means “T can be any type that supports comparison.” Now this works with numbers, strings, and any type that implements PartialOrd:
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest: {}", largest(&numbers)); // 100
let words = vec!["apple", "banana", "cherry"];
println!("Largest: {}", largest(&words)); // cherry
}
Generic Functions
A generic function uses <T> after the function name:
fn first_or_default<T: Default + Clone>(list: &[T]) -> T {
match list.first() {
Some(item) => item.clone(),
None => T::default(),
}
}
This function returns the first element or a default value if the list is empty. The bounds Default + Clone mean:
Default— the type has a default value (0 for numbers, "" for strings)Clone— we can clone the element to return an owned copy
let numbers = vec![10, 20, 30];
println!("{}", first_or_default(&numbers)); // 10
let empty: Vec<i32> = vec![];
println!("{}", first_or_default(&empty)); // 0
Generic Structs
Structs can also be generic:
#[derive(Debug, PartialEq)]
struct Pair<T> {
first: T,
second: T,
}
Both fields must be the same type because they both use T. You can create pairs of any type:
let int_pair = Pair { first: 1, second: 2 };
let str_pair = Pair { first: "hello", second: "world" };
Impl Blocks with Generics
You write methods for generic structs with impl<T>:
impl<T> Pair<T> {
fn new(first: T, second: T) -> Pair<T> {
Pair { first, second }
}
fn swap(self) -> Pair<T> {
Pair {
first: self.second,
second: self.first,
}
}
}
Conditional Methods
You can add methods that only exist when T meets certain trait bounds:
impl<T: fmt::Display + PartialOrd> Pair<T> {
fn larger(&self) -> &T {
if self.first >= self.second {
&self.first
} else {
&self.second
}
}
fn display_larger(&self) -> String {
format!("The larger value is: {}", self.larger())
}
}
larger() and display_larger() only exist for Pair<T> where T implements both Display and PartialOrd. If you try to call Pair::new(vec![1], vec![2]).larger(), it will not compile — Vec does not implement Display.
let pair = Pair::new(10, 20);
println!("{}", pair.display_larger()); // The larger value is: 20
let pair = Pair::new(1, 2).swap();
println!("{:?}", pair); // Pair { first: 2, second: 1 }
Multiple Type Parameters
You can use multiple generic types:
#[derive(Debug, PartialEq)]
struct KeyValue<K, V> {
key: K,
value: V,
}
impl<K: fmt::Display, V: fmt::Display> KeyValue<K, V> {
fn new(key: K, value: V) -> KeyValue<K, V> {
KeyValue { key, value }
}
fn format_entry(&self) -> String {
format!("{}: {}", self.key, self.value)
}
}
fn main() {
let kv = KeyValue::new("name", "Alex");
println!("{}", kv.format_entry()); // name: Alex
let kv2 = KeyValue::new(1, 99.9);
println!("{}", kv2.format_entry()); // 1: 99.9
}
Generic Enums
Enums can also be generic. You already know two important generic enums:
// From the standard library:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
You can create your own:
#[derive(Debug, PartialEq)]
enum Container<T> {
Empty,
Single(T),
Multiple(Vec<T>),
}
impl<T: Clone> Container<T> {
fn count(&self) -> usize {
match self {
Container::Empty => 0,
Container::Single(_) => 1,
Container::Multiple(items) => items.len(),
}
}
fn first(&self) -> Option<&T> {
match self {
Container::Empty => None,
Container::Single(item) => Some(item),
Container::Multiple(items) => items.first(),
}
}
}
let empty: Container<i32> = Container::Empty;
let single = Container::Single(42);
let multi = Container::Multiple(vec![1, 2, 3]);
println!("{}", empty.count()); // 0
println!("{:?}", single.first()); // Some(42)
println!("{}", multi.count()); // 3
Trait Bounds
Trait bounds are how you tell the compiler what a generic type can do. Without bounds, you can only move and drop generic values — nothing else.
Single Bound
fn print_item<T: fmt::Display>(item: &T) {
println!("{}", item);
}
Multiple Bounds
Use + to require multiple traits:
fn print_debug_display<T: fmt::Display + fmt::Debug>(item: &T) {
println!("Display: {}", item);
println!("Debug: {:?}", item);
}
Where Clauses
When bounds get complex, use a where clause:
fn compare_and_display<T, U>(t: &T, u: &U) -> String
where
T: fmt::Display + PartialOrd,
U: fmt::Display + Clone,
{
format!("T={}, U={}", t, u)
}
The where clause is the same as putting bounds in the angle brackets. It is just easier to read. Use where when:
- You have many generic parameters
- Each parameter has multiple bounds
- The function signature would be too long
A Practical Example: Generic Stack
Here is a complete generic data structure:
#[derive(Debug)]
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Stack<T> {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn peek(&self) -> Option<&T> {
self.items.last()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn size(&self) -> usize {
self.items.len()
}
}
This stack works with any type:
let mut int_stack: Stack<i32> = Stack::new();
int_stack.push(10);
int_stack.push(20);
println!("{:?}", int_stack.pop()); // Some(20)
println!("{:?}", int_stack.peek()); // Some(10)
let mut str_stack: Stack<String> = Stack::new();
str_stack.push(String::from("hello"));
str_stack.push(String::from("world"));
println!("{}", str_stack.size()); // 2
Notice that Stack<T> does not require any trait bounds on T. The methods only use Vec operations, which work with any type. You only add bounds when you need to do something specific with T (like printing or comparing).
Monomorphization
How does Rust make generics fast? Through monomorphization. At compile time, Rust looks at every place you use a generic type and creates a specific version for that type.
When you write:
let pair_int = Pair::new(1, 2);
let pair_str = Pair::new("a", "b");
The compiler generates:
// Generated by the compiler — you never see this
struct Pair_i32 { first: i32, second: i32 }
struct Pair_str { first: &str, second: &str }
This means:
- No runtime cost — generics are as fast as writing specific types
- Larger binary — the compiler creates more code
- Compile-time checking — type errors are caught before your program runs
This is different from Java or Python, where generics use type erasure or duck typing. In Rust, generics have zero cost at runtime.
Generic Helper Functions
Here are useful generic functions you might write:
fn wrap_in_vec<T>(item: T) -> Vec<T> {
vec![item]
}
fn repeat<T: Clone>(item: &T, count: usize) -> Vec<T> {
let mut result = Vec::with_capacity(count);
for _ in 0..count {
result.push(item.clone());
}
result
}
let wrapped = wrap_in_vec(42);
println!("{:?}", wrapped); // [42]
let repeated = repeat(&"hi", 3);
println!("{:?}", repeated); // ["hi", "hi", "hi"]
Generics vs Trait Objects
In the traits tutorial, we learned about trait objects (dyn Trait). Both generics and trait objects let you write flexible code. But they work differently:
Generics (static dispatch):
fn print_item<T: fmt::Display>(item: &T) {
println!("{}", item);
}
The compiler creates a separate version of print_item for each type you use it with. This is called monomorphization. The result is fast because there is no indirection — but it increases binary size.
Trait objects (dynamic dispatch):
fn print_item(item: &dyn fmt::Display) {
println!("{}", item);
}
There is only one version of the function. It uses a pointer to look up the right method at runtime. This is slightly slower but keeps binary size small.
When to use which:
- Use generics by default — they are faster and catch errors at compile time
- Use trait objects when you need a collection of different types (
Vec<Box<dyn Trait>>) - Use trait objects when you want to reduce compile times or binary size
- Use trait objects when the concrete type is only known at runtime
Turbofish Syntax
Sometimes the compiler cannot infer the generic type. You can specify it with the turbofish syntax ::<Type>:
let numbers = vec![1, 2, 3, 4, 5];
// The compiler doesn't know what type to collect into
// let result = numbers.iter().collect(); // ERROR
// Tell it with turbofish:
let result = numbers.iter().collect::<Vec<&i32>>();
println!("{:?}", result);
// Or with a type annotation on the variable:
let result: Vec<&i32> = numbers.iter().collect();
Turbofish works on functions and methods:
let parsed = "42".parse::<i32>().unwrap();
println!("{}", parsed); // 42
let five = std::cmp::max::<i32>(3, 5);
println!("{}", five); // 5
The name “turbofish” comes from the ::<> syntax looking like a fish. It is a beloved part of Rust culture.
Common Mistakes
Mistake: Missing Trait Bounds
fn print_item<T>(item: &T) {
println!("{}", item); // ERROR: T might not implement Display
}
Fix: Add the bound:
fn print_item<T: fmt::Display>(item: &T) {
println!("{}", item);
}
Mistake: Too Many Bounds
fn do_something<T: Clone + Debug + Display + PartialEq + PartialOrd + Hash>(item: T) {
// ...
}
If you need this many bounds, consider whether you really need all of them. Sometimes a concrete type or a smaller trait is better.
Mistake: Returning References from Generic Functions
fn first<T>(list: &[T]) -> &T {
&list[0]
}
This works because of lifetime elision. But this does not:
fn create<T: Default>() -> &T { // ERROR: no input reference to borrow from
// Where would the reference point to?
}
Fix: Return an owned value:
fn create<T: Default>() -> T {
T::default()
}
Mistake: Confusing Impl Trait and Generics
These two signatures look similar but behave differently:
// Generic: caller chooses the type
fn process<T: fmt::Display>(item: T) { }
// Impl trait in argument: same as generic (syntactic sugar)
fn process(item: impl fmt::Display) { }
// Impl trait in return: function chooses the type (opaque type)
fn create() -> impl fmt::Display {
42 // The caller does not know this is i32
}
impl Trait in argument position is just sugar for generics. In return position, it hides the concrete type from the caller.
Generics in the Standard Library
You already use generics everywhere in Rust:
// Vec<T> — generic over element type
let numbers: Vec<i32> = vec![1, 2, 3];
let names: Vec<String> = vec![String::from("Alex")];
// HashMap<K, V> — generic over key and value
use std::collections::HashMap;
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alex"), 100);
// Option<T> and Result<T, E>
let maybe: Option<i32> = Some(42);
let result: Result<i32, String> = Ok(42);
Understanding generics helps you read the standard library documentation. When you see fn push(&mut self, value: T) in the docs for Vec<T>, you know that T is the element type.
Summary
| Concept | Syntax | Example |
|---|---|---|
| Generic function | fn name<T>() | fn largest<T: PartialOrd>(list: &[T]) |
| Generic struct | struct Name<T> | struct Pair<T> { first: T, second: T } |
| Generic enum | enum Name<T> | enum Container<T> { Empty, Single(T) } |
| Trait bound | <T: Trait> | <T: Display> |
| Multiple bounds | <T: A + B> | <T: Display + Clone> |
| Where clause | where T: Trait | where T: Display, U: Clone |
| Two type params | <K, V> | struct KeyValue<K, V> |
| Monomorphization | Automatic | Compiler generates specific code |
Source Code
Related Tutorials
- Rust Tutorial #9: Traits — traits define shared behavior
- Rust Tutorial #11: Lifetimes — next tutorial
What’s Next?
We now know traits and generics — two of Rust’s most powerful features. Next, we learn lifetimes — how Rust tracks how long references are valid. Lifetimes are the final piece of Rust’s ownership system. They might seem hard at first, but once you understand them, the borrow checker becomes your friend.