On this page
Generics: zero-cost abstractions
Generics: writing reusable code
Generics allow you to write functions, structs, and enums that work with multiple types without duplicating code. In Rust, generics are a core language feature used throughout the standard library: Vec<T>, Option<T>, Result<T, E>, HashMap<K, V>.
Generic functions
The problem without generics: you need one function per type.
// Without generics: duplicated code
fn maximum_i32(list: &[i32]) -> i32 {
let mut max = list[0];
for &n in list {
if n > max { max = n; }
}
max
}
fn maximum_f64(list: &[f64]) -> f64 {
let mut max = list[0];
for &n in list {
if n > max { max = n; }
}
max
}With generics, a single function handles any comparable type:
// T is the generic type parameter
// PartialOrd: T must support > and < comparisons
// Copy: T must be copyable (to assign max = list[0])
fn maximum<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut max = list[0];
for &item in list.iter() {
if item > max {
max = item;
}
}
max
}
fn main() {
let integers = vec![34, 50, 25, 100, 65];
println!("Maximum integer: {}", maximum(&integers));
let floats = [2.5, 1.7, 9.3, 4.1, 7.8];
println!("Maximum float: {}", maximum(&floats));
let chars = ['y', 'm', 'a', 'q'];
println!("Maximum char: {}", maximum(&chars));
}Generic structs
#[derive(Debug, Clone)]
struct Stack<T> {
elements: Vec<T>,
max_capacity: usize,
}
impl<T> Stack<T> {
fn new(capacity: usize) -> Self {
Stack {
elements: Vec::with_capacity(capacity),
max_capacity: capacity,
}
}
fn push(&mut self, value: T) -> Result<(), &'static str> {
if self.elements.len() >= self.max_capacity {
return Err("Stack is full");
}
self.elements.push(value);
Ok(())
}
fn pop(&mut self) -> Option<T> {
self.elements.pop()
}
fn peek(&self) -> Option<&T> {
self.elements.last()
}
fn is_empty(&self) -> bool {
self.elements.is_empty()
}
fn len(&self) -> usize {
self.elements.len()
}
}
// Implementation specific to types implementing Display
impl<T: std::fmt::Display> Stack<T> {
fn display(&self) {
print!("[Top] ");
for el in self.elements.iter().rev() {
print!("{el} ");
}
println!("[Bottom]");
}
}
fn main() {
let mut num_stack: Stack<i32> = Stack::new(5);
num_stack.push(1).unwrap();
num_stack.push(2).unwrap();
num_stack.push(3).unwrap();
num_stack.display();
println!("Pop: {:?}", num_stack.pop());
println!("Peek: {:?}", num_stack.peek());
let mut text_stack: Stack<&str> = Stack::new(3);
text_stack.push("hello").unwrap();
text_stack.push("world").unwrap();
text_stack.display();
}Generic enums
You already know the most important generic enums from the std:
// As defined in the standard library:
// enum Option<T> { Some(T), None }
// enum Result<T, E> { Ok(T), Err(E) }
// You can also create generic enums:
#[derive(Debug)]
enum Tree<T> {
Leaf(T),
Node {
value: T,
left: Box<Tree<T>>,
right: Box<Tree<T>>,
},
}
impl<T: std::fmt::Display + PartialOrd> Tree<T> {
fn contains(&self, target: &T) -> bool {
match self {
Tree::Leaf(v) => v == target,
Tree::Node { value, left, right } => {
value == target
|| left.contains(target)
|| right.contains(target)
}
}
}
}
fn main() {
let tree = Tree::Node {
value: 10,
left: Box::new(Tree::Node {
value: 5,
left: Box::new(Tree::Leaf(3)),
right: Box::new(Tree::Leaf(7)),
}),
right: Box::new(Tree::Leaf(15)),
};
println!("Contains 7? {}", tree.contains(&7));
println!("Contains 6? {}", tree.contains(&6));
}Multiple bounds with where
When bounds get complex, the where clause improves readability:
use std::fmt::{Debug, Display};
use std::ops::Add;
// Without where (hard to read):
fn process<T: Display + Debug + Clone + PartialOrd>(value: T) -> T { value }
// With where (much clearer):
fn serialize<T>(collection: &[T]) -> String
where
T: Display + Debug + Clone,
{
collection.iter()
.map(|item| format!("{item}"))
.collect::<Vec<_>>()
.join(", ")
}
// Multiple generic parameters with different bounds
fn blend<A, B, C>(a: A, b: B) -> C
where
A: Into<C>,
B: Into<C>,
C: Add<Output = C>,
{
a.into() + b.into()
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
println!("{}", serialize(&numbers));
let words = ["hello", "world", "rust"];
println!("{}", serialize(&words));
// blend i32 + i32 → i64
let result: i64 = blend(10_i32, 20_i32);
println!("Result: {result}");
}Monomorphization: why generics are free
When the compiler compiles generic code, it generates a specialized version for each concrete type used. This process is called monomorphization:
fn identity<T>(value: T) -> T { value }
fn main() {
let n = identity(42_i32); // Compiler generates identity_i32
let s = identity("hello"); // Compiler generates identity_str
let f = identity(3.14_f64); // Compiler generates identity_f64
println!("{n} {s} {f}");
}The resulting binary contains optimized code specific to each type — exactly as if you had written three separate functions. No vtable overhead, no boxing, no runtime costs.
This contrasts with languages like Java/C# where generics use boxing and can have overhead, or older Go versions where generics were not available.
Turbofish: explicitly specifying types
Sometimes the compiler cannot infer the generic type. Use the "turbofish" syntax ::<T>:
fn main() {
// Without turbofish — compiler needs context
let numbers: Vec<i32> = Vec::new();
// With turbofish on the method
let v = Vec::<i32>::new();
// In generic function calls
let n = "42".parse::<i32>().unwrap();
let f = "3.14".parse::<f64>().unwrap();
// In collect
let squares = (1..=5).map(|n| n * n).collect::<Vec<_>>();
println!("{:?}", squares);
// In from
let s = String::from("hello");
let v2 = Vec::<u8>::from(s.as_bytes());
println!("{:?}", v2);
}Generics in impl blocks: conditional implementations
You can implement methods only for certain generic types:
use std::fmt::Display;
struct Wrapped<T> {
value: T,
}
impl<T> Wrapped<T> {
fn new(value: T) -> Self {
Wrapped { value }
}
fn get(&self) -> &T {
&self.value
}
}
// Only available when T implements Display
impl<T: Display> Wrapped<T> {
fn show(&self) {
println!("Wrapped: {}", self.value);
}
}
// Only available when T implements Clone
impl<T: Clone> Wrapped<T> {
fn duplicate(&self) -> (T, T) {
(self.value.clone(), self.value.clone())
}
}
fn main() {
let w = Wrapped::new(42);
w.show(); // available: i32 impl Display
let (a, b) = w.duplicate(); // available: i32 impl Clone
println!("Duplicated: {a}, {b}");
let w2 = Wrapped::new(vec![1, 2, 3]);
// w2.show(); // Vec<i32> implements Debug but not Display by default
let (v1, v2) = w2.duplicate(); // available: Vec impl Clone
println!("{:?} {:?}", v1, v2);
}With a solid understanding of generics and traits, you are ready for the error handling lesson — where Result<T, E> and the ? operator shine at full power.
// Generic function: T must implement PartialOrd and Copy
fn maximum<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut max = list[0];
for &item in list.iter() {
if item > max {
max = item;
}
}
max
}
// Generic struct
#[derive(Debug)]
struct Pair<T, U> {
first: T,
second: U,
}
impl<T: std::fmt::Display, U: std::fmt::Display> Pair<T, U> {
fn new(first: T, second: U) -> Self {
Pair { first, second }
}
fn show(&self) {
println!("({}, {})", self.first, self.second);
}
}
// Implementation only for pairs of the same type
impl<T: std::fmt::Display + PartialOrd> Pair<T, T> {
fn larger(&self) -> &T {
if self.first > self.second { &self.first } else { &self.second }
}
}
fn main() {
let integers = vec![34, 50, 25, 100, 65];
println!("Maximum: {}", maximum(&integers));
let floats = [2.5, 1.7, 9.3, 4.1];
println!("Maximum: {}", maximum(&floats));
let p1 = Pair::new("hello", 42);
p1.show();
let p2 = Pair::new(10_i32, 20_i32);
println!("Larger: {}", p2.larger());
}
Sign in to track your progress