When I first started writing Rust, I kept running into the same annoying problem: I’d write a function to find the largest number in a list of i32 values, and then five minutes later I’d need the exact same logic for f64 values, or char values, or my own custom struct. Copy-pasting the same function three or four times with only the type signature changed felt wrong. That’s exactly the itch generics scratch, and once it clicked for me, it genuinely changed how I structure Rust code.
In this article, I’m going to walk you through generics in Rust from the ground up — what they are, why the compiler doesn’t punish you for using them, how to write generic functions and structs, and how all of this ties into ownership, borrowing, and performance. I’ll use real code you can copy into a project and run yourself.
What Problem Are Generics Actually Solving?
Let me show you the pain point first. Suppose I write this function to find the largest value in a slice of integers:
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
Now I need the same thing for char:
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
Notice the body is identical. Only the type changed. This is the definition of duplicated logic, and duplicated logic is a maintenance liability — if I find a bug in one, I have to remember to fix it in the other. Generics let me write this once and let the compiler generate the specialized versions for me.
Defining a Generic Function
Here’s the generic version:
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 number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
Running this with cargo run gives me:
The largest number is 100
The largest char is y
Let me break down the syntax piece by piece, because the angle-bracket notation trips a lot of beginners up:
<T: PartialOrd + Copy>declares a type parameterTright after the function name. This is called a generic type parameter, and by convention Rust programmers use single uppercase letters likeT,U, orVfor these — it’s a stylistic convention borrowed from C++ templates and Haskell.PartialOrd + Copyare trait bounds. I’m telling the compiler “whatever typeTends up being, it must implementPartialOrd(so I can compare with>) andCopy(so I can assign it without moving it out of the slice).” Without these bounds, the compiler would reject the function outright because it can’t assume every possible type supports comparison or copying.- The parameter
list: &[T]and the return typeTboth use the same type parameter, which tells the compiler that whatever type goes in is the same type that comes out.
I want to be clear about something important here: this isn’t like generics (or templates) in some other languages where you pay a runtime cost. Rust uses a technique called monomorphization. At compile time, the compiler looks at every concrete type largest is called with — here, i32 and char — and generates a separate, fully specialized version of the function for each one. By the time your code is running, there’s no generic function anymore; there are just two ordinary, statically-typed functions that happen to have been generated from one template. This means generics in Rust cost you nothing at runtime. You get the code reuse of generics with the speed of hand-written, type-specific code.
Defining Generic Structs
Generics aren’t limited to functions — structs benefit from them just as much. Say I want a Point struct that can hold either integer or floating-point coordinates:
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 };
println!("Integer point: ({}, {})", integer_point.x, integer_point.y);
println!("Float point: ({}, {})", float_point.x, float_point.y);
}
Output:
Integer point: (5, 10)
Float point: (1, 4)
Here, T must be the same type for both x and y — I can’t create Point { x: 5, y: 4.0 } because 5 is an i32 and 4.0 is an f64, and the compiler will refuse to unify them under one T. If I actually need mixed types, I define two type parameters:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 4.0 };
println!("Mixed point: ({}, {})", mixed.x, mixed.y);
}
This compiles fine because x is bound to T and y is bound to U, two independent type parameters.
Implementing Methods on Generic Structs
You can also write generic implementation blocks. This is where I see a lot of newcomers get confused about where the <T> goes:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
The impl<T> Point<T> line declares T as generic right after impl, then uses it to specify that we’re implementing methods on Point<T> for any T. You can also constrain methods to specific concrete types. For example, I might want a method that only exists when T is f32:
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Now distance_from_origin only exists on Point<f32> instances, not on Point<i32> or Point<String>. This lets me mix generic and type-specific behavior in the same codebase.
Ownership and Borrowing With Generics
Generics interact with Rust’s ownership system in ways worth calling out explicitly. When I wrote fn largest<T: PartialOrd + Copy>(list: &[T]) -> T, I deliberately borrowed the slice with &[T] rather than taking ownership with Vec<T>. If I’d taken ownership, calling largest(number_list) would move number_list into the function, and I wouldn’t be able to use number_list again afterward in main. Borrowing avoids that problem — the caller keeps ownership, and my function just reads.
The Copy bound matters here too. Inside the loop, let mut largest = list[0] and later largest = item copy values out of the borrowed slice. If T were something like String, which doesn’t implement Copy, this code wouldn’t compile — you can’t copy a String implicitly, only clone it explicitly. In that case, I’d need to either add a Clone bound and call .clone(), or rewrite the function to work with references (&T) throughout instead of owned values. This is a very typical decision point in Rust generic code: do I want my generic function to work only with cheaply-copyable types, or do I want it to be maximally flexible and just borrow everything?
Real-World Applications
Generics show up constantly in idiomatic Rust:
Option<T>andResult<T, E>— the standard library’s error-handling types are themselves generic structs/enums, letting you haveOption<i32>,Option<String>,Result<File, io::Error>, and so on, all sharing one implementation.- Collections —
Vec<T>,HashMap<K, V>,HashSet<T>are all generic, which is why you can have aVec<u8>for raw bytes and aVec<MyStruct>for domain objects using the exact same type. - Custom data structures — if you’re building something like a binary tree, a linked list, or a cache, generics let you write the structural logic once and reuse it for any payload type.
- Builder patterns and wrapper types — I frequently define a generic
Wrapper<T>when I need to attach extra behavior (like logging or validation) to an arbitrary inner type without duplicating code per type.
Best Practices and Idiomatic Patterns
A few things I’ve learned to do consistently:
- Keep trait bounds minimal. Only require what the function body actually needs. If you don’t call
.clone(), don’t bound onClone. - Prefer
whereclauses for readability when bounds get long:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: std::fmt::Display + Clone,
U: Clone + std::fmt::Debug,
{
// ...
0
}
- Use descriptive names for complex generic code.
TandUare fine for simple cases, but if you have five type parameters, name them something likeKey,Value,Error. - Let monomorphization work for you — don’t be afraid of generics for performance reasons; Rust generics compile down to specialized, non-generic machine code.
Common Mistakes and Debugging Tips
The most common error I see (and made myself constantly when starting out) is forgetting a trait bound and getting a message like:
error[E0369]: binary operation `>` cannot be applied to type `T`
The fix is almost always adding the right bound — in this case T: PartialOrd. Read the compiler’s suggestion carefully; recent versions of rustc often tell you exactly which trait to add.
Another frequent mistake is trying to move a value out of a generic reference without a Copy or Clone bound, producing:
error[E0507]: cannot move out of `*item` which is behind a shared reference
That’s your cue to either add T: Clone and call .clone(), or restructure the function to operate on references instead of owned values.
FAQs
Do generics slow down my program? No. Due to monomorphization, generic code compiles to the same machine code as if you’d hand-written a version for each concrete type. The tradeoff is larger binary size (since each instantiation is duplicated in the compiled output), not runtime speed.
What’s the difference between generics and trait objects (dyn Trait)? Generics are resolved at compile time (static dispatch) and produce zero-cost abstractions but require knowing types at compile time. Trait objects use dynamic dispatch through a vtable, incurring a small runtime cost, but let you store different concrete types behind one interface at runtime.
Can I have default type parameters? Yes, using syntax like struct Foo<T = i32> { ... }, though this is less common outside operator overloading traits like Add.
Why do I need trait bounds at all if the compiler can just check per-instantiation? Because Rust type-checks generic code once, before knowing the concrete type. Bounds are the contract that lets the compiler verify your generic body is valid for any type that could ever be substituted in.
Summary
Generics are one of Rust’s core tools for writing reusable, type-safe code without sacrificing performance. By parameterizing functions and structs over types, you eliminate duplicated logic, and thanks to monomorphization, you never pay a runtime tax for the abstraction. Combined with trait bounds, generics give you precise control over what capabilities a type parameter must have, and combined with Rust’s ownership and borrowing rules, they force you to be explicit about whether your generic code owns, borrows, or copies its data.
References
- The Rust Programming Language Book, Chapter 10: Generic Types, Traits, and Lifetimes — https://doc.rust-lang.org/book/ch10-00-generics.html
- The Rust Reference, Generics — https://doc.rust-lang.org/reference/items/generics.html
- Rust by Example, Generics — https://doc.rust-lang.org/rust-by-example/generics.html
- Cargo Documentation — https://doc.rust-lang.org/cargo/
