Generics
Overview
Generics let you write code that works with multiple types while maintaining full type safety. Instead of writing separate functions for each type, you write one function that works with any type meeting certain requirements.
flowchart LR
subgraph "Without Generics"
A["largest_i32(list: &[i32])"]
B["largest_f64(list: &[f64])"]
C["largest_char(list: &[char])"]
end
subgraph "With Generics"
D["largest<T>(list: &[T])"]
end
A -.-> D
B -.-> D
C -.-> D
style D fill:#90EE90
Key insight: Generics are a zero-cost abstraction. The compiler generates specialized code for each concrete type (monomorphization), so generic code runs as fast as hand-written specific code.
When to Use Generics
| Scenario | Use Generics? | Example |
|---|---|---|
| Same logic, different types | ✓ Yes | Vec<T>, Option<T> |
| Need type flexibility | ✓ Yes | fn swap<T>(a: &mut T, b: &mut T) |
| Algorithm independent of type | ✓ Yes | fn sort<T: Ord>(list: &mut [T]) |
| Type-specific behavior | ✗ No | Use trait with implementations |
| Known fixed type | ✗ No | Just use the concrete type |
flowchart TD
A{Same logic for<br/>different types?} -->|Yes| B[Use Generics]
A -->|No| C[Use concrete type]
B --> D{Need constraints?}
D -->|Yes| E["Add trait bounds<br/>T: Trait"]
D -->|No| F["Plain generic<br/>fn foo<T>()"]
style B fill:#90EE90
style E fill:#FFE4B5
The Problem Generics Solve
Without generics, you’d duplicate code:
// Duplicated logic for each type!
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
// Imagine doing this for every type...
Generic Functions
One function works for all types:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest char: {}", largest(&chars));
}
flowchart TD
subgraph "Generic Function"
F["fn largest<T: PartialOrd>(list: &[T]) -> &T"]
end
subgraph "Compiler Generates"
I["largest_i32(list: &[i32]) -> &i32"]
C["largest_char(list: &[char]) -> &char"]
FL["largest_f64(list: &[f64]) -> &f64"]
end
F -->|"monomorphization"| I
F -->|"monomorphization"| C
F -->|"monomorphization"| FL
Generic Structs
// Single type parameter
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
// Error: x and y must be same type
// let mixed = Point { x: 5, y: 4.0 };
}
Multiple Type Parameters
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 4.0 }; // Point<i32, f64>
let both_int = Point { x: 1, y: 2 }; // Point<i32, i32>
let both_str = Point { x: "a", y: "b" }; // Point<&str, &str>
}
flowchart TD
subgraph "Point<T>"
P1["x: T, y: T"]
P1 --> E1["x and y same type"]
end
subgraph "Point<T, U>"
P2["x: T, y: U"]
P2 --> E2["x and y can differ"]
end
Generic Enums
Standard library uses generics extensively:
// Option: value may or may not exist
enum Option<T> {
Some(T),
None,
}
// Result: operation may succeed or fail
enum Result<T, E> {
Ok(T),
Err(E),
}
fn main() {
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
let success: Result<i32, String> = Ok(42);
let failure: Result<i32, String> = Err("error".to_string());
}
Generic Methods
struct Point<T> {
x: T,
y: T,
}
// Methods for any T
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
// Methods only for specific types
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point::new(3.0, 4.0);
println!("Distance: {}", p.distance_from_origin()); // 5.0
let p_int = Point::new(3, 4);
// p_int.distance_from_origin(); // Error! Only for f64
}
flowchart TD
subgraph "impl<T> Point<T>"
M1["x(), y(), new()"]
M1 --> A1["Available for ALL Point<T>"]
end
subgraph "impl Point<f64>"
M2["distance_from_origin()"]
M2 --> A2["Only for Point<f64>"]
end
style A1 fill:#90EE90
style A2 fill:#FFE4B5
Mixed Generic Parameters
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
// Method introduces its own generic parameters
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x, // Takes x from self (type T)
y: other.y, // Takes y from other (type W)
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 }; // Point<i32, f64>
let p2 = Point { x: "Hello", y: 'c' }; // Point<&str, char>
let p3 = p1.mixup(p2); // Point<i32, char>
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
Trait Bounds
Constrain generics to types with specific capabilities:
use std::fmt::Display;
// T must implement Display to be printed
fn print_pair<T: Display, U: Display>(a: T, b: U) {
println!("({}, {})", a, b);
}
// T must be comparable AND displayable
fn print_largest<T: PartialOrd + Display>(a: T, b: T) {
let largest = if a > b { a } else { b };
println!("Largest: {}", largest);
}
flowchart TD
A["fn foo<T>(x: T)"] --> B["T can be ANY type"]
C["fn foo<T: Display>(x: T)"] --> D["T must impl Display"]
E["fn foo<T: Display + Clone>(x: T)"] --> F["T must impl BOTH"]
style B fill:#FFB6C1
style D fill:#FFE4B5
style F fill:#90EE90
where Clauses
For complex bounds, use where for readability:
use std::fmt::{Debug, Display};
// Hard to read
fn complex<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
// ...
}
// Cleaner with where
fn complex<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}
Conditional Implementation
Implement methods only when bounds are met:
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
// Always available
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
// Only when T is comparable AND displayable
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("Largest: {}", self.x);
} else {
println!("Largest: {}", self.y);
}
}
}
fn main() {
let pair = Pair::new(5, 10);
pair.cmp_display(); // Works - i32 impls Display + PartialOrd
let pair2 = Pair::new(vec![1], vec![2]);
// pair2.cmp_display(); // Error - Vec doesn't impl Display
}
Blanket Implementations
Implement a trait for ALL types meeting a bound:
use std::fmt::Display;
// From standard library: any Display type gets ToString
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
// Now EVERY Display type has .to_string()
let s = 42.to_string(); // "42"
let s = 3.14.to_string(); // "3.14"
let s = true.to_string(); // "true"
flowchart TD
A["impl<T: Display> ToString for T"] --> B["Every Display type"]
B --> C["Automatically gets ToString"]
D["i32: Display"] --> C
E["f64: Display"] --> C
F["bool: Display"] --> C
G["String: Display"] --> C
style C fill:#90EE90
Const Generics
Generic over constant values (Rust 1.51+):
// Array with generic size
struct Array<T, const N: usize> {
data: [T; N],
}
impl<T: Default + Copy, const N: usize> Array<T, N> {
fn new() -> Self {
Array {
data: [T::default(); N],
}
}
fn len(&self) -> usize {
N // Const generic is a compile-time constant
}
}
fn main() {
let arr5: Array<i32, 5> = Array::new();
let arr10: Array<f64, 10> = Array::new();
println!("arr5 length: {}", arr5.len()); // 5
println!("arr10 length: {}", arr10.len()); // 10
}
Generic Lifetimes
Combine type and lifetime parameters:
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn new(text: &'a str) -> Self {
Excerpt { part: text }
}
fn part(&self) -> &str {
self.part
}
}
// Type generics + lifetime generics
fn longest_with_announcement<'a, T: Display>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str {
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}
Turbofish Syntax (::<>)
Explicitly specify type parameters:
fn main() {
// Type inference
let v: Vec<i32> = Vec::new();
// Turbofish - explicit type
let v = Vec::<i32>::new();
// Useful with parse
let n: i32 = "42".parse().unwrap(); // Type annotation
let n = "42".parse::<i32>().unwrap(); // Turbofish
// With collect
let nums: Vec<i32> = (0..5).collect(); // Type annotation
let nums = (0..5).collect::<Vec<i32>>(); // Turbofish
}
Phantom Types
Type parameter not used in fields, but affects type:
use std::marker::PhantomData;
// Distance with unit type
struct Distance<Unit> {
value: f64,
_marker: PhantomData<Unit>,
}
struct Meters;
struct Feet;
impl<U> Distance<U> {
fn new(value: f64) -> Self {
Distance { value, _marker: PhantomData }
}
}
fn main() {
let d1: Distance<Meters> = Distance::new(100.0);
let d2: Distance<Feet> = Distance::new(328.0);
// Can't accidentally mix units!
// let sum = d1.value + d2.value; // Would be wrong semantically
// But Rust doesn't prevent this automatically...
// Type-safe operations require explicit conversion
}
Monomorphization: Zero-Cost Abstraction
The compiler generates specific code for each type used:
// Your generic code
fn largest<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
let x = largest(5, 10); // Uses i32
let y = largest(1.0, 2.0); // Uses f64
flowchart TD
subgraph "Your Code"
G["fn largest<T: PartialOrd>(a: T, b: T) -> T"]
end
subgraph "Compiler Generates"
I["fn largest_i32(a: i32, b: i32) -> i32"]
F["fn largest_f64(a: f64, b: f64) -> f64"]
end
G -->|"monomorphization"| I
G -->|"monomorphization"| F
subgraph "Result"
R["Zero runtime overhead<br/>Same as hand-written code"]
end
I --> R
F --> R
style R fill:#90EE90
Generics have zero runtime cost—they’re as fast as writing type-specific functions by hand.
Common Generic Patterns
Pattern 1: Generic Constructor
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
fn new(value: T) -> Self {
Wrapper { value }
}
fn into_inner(self) -> T {
self.value
}
}
Pattern 2: Generic Conversion
impl<T, U> From<Wrapper<T>> for Wrapper<U>
where
U: From<T>,
{
fn from(wrapper: Wrapper<T>) -> Self {
Wrapper {
value: U::from(wrapper.value),
}
}
}
Pattern 3: Builder with Generics
struct Builder<State> {
value: i32,
_state: PhantomData<State>,
}
struct Initial;
struct Configured;
impl Builder<Initial> {
fn new() -> Self {
Builder { value: 0, _state: PhantomData }
}
fn configure(self, value: i32) -> Builder<Configured> {
Builder { value, _state: PhantomData }
}
}
impl Builder<Configured> {
fn build(self) -> i32 {
self.value
}
}
// Can only build after configuring!
let result = Builder::new()
.configure(42)
.build();
Pattern 4: Default Type Parameters
struct Container<T, Allocator = DefaultAllocator> {
data: Vec<T>,
_allocator: PhantomData<Allocator>,
}
// Usually use default
let c: Container<i32> = Container::default();
// Can specify custom allocator
let c: Container<i32, CustomAllocator> = Container::default();
Summary
mindmap
root((Generics))
Syntax
fn foo<T>
struct Foo<T>
impl<T> Foo<T>
Bounds
T: Trait
T: A + B
where clause
Features
Const generics
Lifetimes
Phantom types
Monomorphization
Zero cost
Compile-time
Type-specific code
| Feature | Syntax | Example |
|---|---|---|
| Generic function | fn foo<T>() |
fn swap<T>(a: &mut T, b: &mut T) |
| Generic struct | struct S<T> |
struct Point<T> { x: T, y: T } |
| Generic enum | enum E<T> |
enum Option<T> { Some(T), None } |
| Trait bound | T: Trait |
fn print<T: Display>(x: T) |
| Multiple bounds | T: A + B |
T: Clone + Debug |
| Where clause | where T: Trait |
Cleaner complex bounds |
| Const generic | const N: usize |
struct Array<T, const N: usize> |
| Turbofish | ::<Type> |
"42".parse::<i32>() |
Exercises
- Create a generic
Stack<T>with push, pop, and peek methods - Implement a generic
minfunction for any type that implementsPartialOrd - Create a
Matrix<T, const ROWS: usize, const COLS: usize>type with basic operations
See Also
- Traits - Defining shared behavior
- Advanced Traits - Associated types and advanced patterns
- Advanced Types - Newtype pattern and type aliases
- Example Code
Next Steps
Congratulations! You’ve completed Part 2: Fundamentals. Continue to Part 3: Intermediate to learn about collections, iterators, and concurrency.