Generics in Rust
Writing code once for many types, with zero runtime cost, using type parameters and trait bounds together.
3 min read
Generics let you write a function, struct, or enum once and have it work over many types, instead of duplicating the same logic for i32, f64, String, and everything else.
The problem generics solve
Without generics, finding the largest value in a list of numbers and the largest in a list of characters would need two near-identical functions:
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
}Identical logic, different types — exactly the kind of duplication generics exist to eliminate.
A generic function
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![10, 25, 3, 47, 8];
println!("{}", largest(&numbers)); // 47
let chars = vec!['y', 'm', 'a', 'q'];
println!("{}", largest(&chars)); // y
}T is a type parameter — a placeholder standing in for whatever concrete type is used at each call site. The trait bound T: PartialOrd + Copy matters here: largest needs to compare values with > (which requires PartialOrd) and copy them into largest (which requires Copy). Without those bounds, the compiler would reject the function — it has no guarantee an arbitrary T supports comparison or copying. This is exactly why the traits lesson came right before this one: generics without bounds can barely do anything, since the compiler won't assume behavior it can't verify.
Generic structs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.5, y: 4.2 };
}Both fields share the same type parameter T here, so Point { x: 5, y: 4.2 } won't compile — mixing an i32 and an f64 in one instance. Using two separate parameters allows that, if it's actually what you want:
struct Point<T, U> {
x: T,
y: U,
}
let mixed = Point { x: 5, y: 4.2 }; // fine — x: i32, y: f64Generic methods
impl<T> Point<T, T> {
fn x(&self) -> &T {
&self.x
}
}Zero-cost abstraction
The detail that makes generics genuinely different from, say, generics in Java: Rust monomorphizes generic code at compile time. For every concrete type largest is actually called with, the compiler generates a separate, specialized version of the function — largest::<i32>, largest::<char>, and so on — as if you'd hand-written each one. There's no runtime type-checking, no boxing, no indirection added on top. You get the ergonomics of writing the logic once, and the runtime performance of having written it separately for every type, which is exactly the "zero-cost abstraction" Rust is known for.
You've now seen the core building blocks — ownership, structs, enums, traits, and generics — that every real Rust program is built from. The remaining lessons in this course move from language features to the ecosystem around them: managing dependencies with Cargo, understanding lifetimes more precisely, and the frameworks built on top of everything covered so far.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.