Generics
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and generics let you write code that works over many types without giving up any of that speed.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll write generic functions and structs, add trait bounds, and see how Rust compiles them to specialized, zero-cost code.
What You'll Learn in This Lesson
- Declaring a type parameter with <T>
- Writing a generic function
- Trait bounds like T: PartialOrd
- Generic structs with one or more parameters
- Generic methods in impl<T> blocks
- Why generics are zero-cost at runtime
1️⃣ Generic Functions and Trait Bounds
A generic function declares a type parameter in angle brackets, like <T>, then uses T in its signature. When the body needs an operation — comparing values, say — you add a trait bound like T: PartialOrd to require that capability.
// A generic function works for many types. <T> declares a type parameter.
// The bound T: PartialOrd means "T must be comparable with <, > etc."
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut biggest = &list[0];
for item in list {
if item > biggest { // allowed because T: PartialOrd
biggest = item;
}
}
biggest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("largest number: {}", largest(&numbers)); // -> 100
let chars = vec!['y', 'm', 'a', 'q'];
println!("largest char: {}", largest(&chars)); // -> y
// The SAME function handled i32 and char with no duplication.
let floats = vec![1.5, 9.2, 3.3];
println!("largest float: {}", largest(&floats)); // -> 9.2
}One function handled integers, chars, and floats. The bound T: PartialOrd is what makes the > comparison legal — it promises every T can be ordered.
2️⃣ Generic Structs and Methods
Structs can be generic too. Point<T> holds two values of the same type; Pair<T, U> lets the fields differ. Methods go in an impl<T> block, optionally with their own bounds — here T: Display so we can print the fields.
// Structs can be generic too. Point<T> holds two values of the SAME type T.
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
// impl<T> means the methods are available for every Point<T>.
impl<T: std::fmt::Display> Point<T> {
fn show(&self) {
println!("({}, {})", self.x, self.y);
}
}
// Two type parameters let the fields differ: Pair<T, U>.
#[derive(Debug)]
struct Pair<T, U> {
first: T,
second: U,
}
fn main() {
let int_point = Point { x: 1, y: 2 };
let float_point = Point { x: 1.5, y: 4.0 };
int_point.show(); // -> (1, 2)
float_point.show(); // -> (1.5, 4)
let mixed = Pair { first: "age", second: 30 };
println!("{:?}", mixed); // -> Pair { first: "age", second: 30 }
}This is exactly how the standard library is built: Vec<T>, Option<T>, and Result<T, E> are all generic types you've already been using.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the generic function, then run it.
// This should return the FIRST element of any slice as a reference.
// 1) Declare the type parameter in the angle brackets.
fn first<___>(list: &[T]) -> &T { // 👉 the type parameter name, T
&list[0]
}
fn main() {
let nums = vec![10, 20, 30];
let words = vec!["a", "b", "c"];
// 2) Call it on both — the same function handles different types.
println!("first num: {}", first(&nums)); // expected: first num: 10
println!("first word: {}", first(&___)); // 👉 the words vector
}
// ✅ Expected output:
// first num: 10
// first word: aMini-Challenge: A Generic Swapper
Write a generic function that swaps the two elements of a same-typed tuple. Run it with cargo run.
// 🎯 MINI-CHALLENGE: a generic pair swapper.
//
// 1. Write fn swap<T>(pair: (T, T)) -> (T, T) that returns the tuple with its
// two elements swapped: (pair.1, pair.0).
// 2. In main, call swap((1, 2)) and print the result with {:?}.
// 3. Call swap(("left", "right")) and print that too — same function, new type.
//
// ✅ Example output:
// (2, 1)
// ("right", "left")
fn main() {
// your code here
}Common Errors (and the fix)
- "binary operation `>` cannot be applied to type `T`" — Add the bound T: PartialOrd so comparisons are allowed.
- "`T` doesn't implement `Display`" — To print a generic with {}, bound it with T: std::fmt::Display.
- Forgetting <T> after impl — Write impl<T> Point<T>; the parameter must be declared after impl.
- Using one parameter for differing fields — If two fields can be different types, use two parameters: Pair<T, U>.
Pro Tips
- 💡 Add bounds only as needed: require just the traits your function actually uses, no more.
- 💡 Use a where clause for many bounds: where T: Display + Clone keeps complex signatures readable.
- 💡 Generics are zero-cost: monomorphization means no runtime penalty — write them freely.
📋 Quick Reference — Generics
| Goal | Rust Syntax |
|---|---|
| Generic fn | fn f<T>(x: T) { } |
| Trait bound | fn f<T: PartialOrd>(x: T) |
| Generic struct | struct P<T> { x: T } |
| Generic impl | impl<T> P<T> { } |
| Two parameters | struct Pair<T, U> |
| where clause | where T: Display |
🎉 Lesson Complete!
- ✅ Generics let one piece of code work over many types
- ✅ Declare type parameters with <T>; constrain them with trait bounds
- ✅ Functions, structs, and methods can all be generic
- ✅ Monomorphization makes generics zero-cost at runtime
Practice quiz
What main problem do generics solve?
- Slower runtime
- Memory leaks
- Code duplication across types
- Missing semicolons
Answer: Code duplication across types. Generics let one piece of code work over many types instead of duplicating it.
How do you declare a type parameter on a function?
- fn f<T>(x: T)
- fn f(T x)
- fn f[T](x)
- fn<T> f(x)
Answer: fn f<T>(x: T). A type parameter goes in angle brackets, like fn f<T>(x: T).
What is a trait bound like T: PartialOrd?
- A runtime check
- A default value
- A lifetime
- A requirement that T implements a trait
Answer: A requirement that T implements a trait. It constrains T to types implementing the trait, here enabling comparisons.
Which bound lets you compare values with the > operator?
- T: Display
- T: PartialOrd
- T: Clone
- T: Copy
Answer: T: PartialOrd. PartialOrd provides ordering comparisons like > and <.
Do generics slow your program down at runtime?
- No, they are zero-cost
- Yes, significantly
- Only with floats
- Only in debug builds
Answer: No, they are zero-cost. Monomorphization makes generics zero-cost at runtime.
What is monomorphization?
- Boxing every value
- Garbage collection
- Generating a specialized copy per concrete type at compile time
- Runtime type checking
Answer: Generating a specialized copy per concrete type at compile time. The compiler creates a fully optimized copy for each concrete type used.
How do you write a struct generic over two types?
- struct Pair<T, T>
- struct Pair<T, U>
- struct Pair[T][U]
- struct Pair(T, U)
Answer: struct Pair<T, U>. Two independent parameters are written struct Pair<T, U>.
Where must the type parameter be declared for a generic impl block?
- After the type name
- Inside the method
- It is inferred
- After the impl keyword, as impl<T>
Answer: After the impl keyword, as impl<T>. Write impl<T> Point<T>; the parameter is declared after impl.
Which bound is needed to print a generic value with {}?
- T: PartialOrd
- T: std::fmt::Display
- T: Iterator
- T: Send
Answer: T: std::fmt::Display. Printing with {} requires the Display trait bound.
Which standard-library types are generic?
- Only Vec
- Only String
- Vec<T>, Option<T>, and Result<T, E>
- None of them
Answer: Vec<T>, Option<T>, and Result<T, E>. Vec, Option, and Result are all generic types built the same way.
Continue this course
- Previous: Modules & Crates
- Next: Traits — Define shared behaviour and program to interfaces with trait bounds
- Quick reference: Rust cheat sheet › Traits & Generics
Frequently asked questions
What problem do generics solve?
Generics let you write a single piece of code that works for many different types, instead of duplicating it for each one. Without generics you might write largest_i32, largest_char, and largest_f64 separately. With a generic function largest<T>, you write the logic once and it works for any type that meets the requirements. This reduces duplication, keeps behaviour consistent, and makes code easier to maintain — the same idea powers Vec<T>, Option<T>, and Result<T, E> in the standard library.
What is a trait bound like T: PartialOrd?
A trait bound constrains a generic type parameter to types that implement a particular trait, which guarantees the operations you use are available. For example, comparing values with > requires the PartialOrd trait, so a function that compares its inputs must declare T: PartialOrd. Without the bound, the compiler would reject the comparison because not every possible type supports it. Trait bounds are how generics stay type-safe: you state what capabilities T must have, and the compiler enforces it.
Do generics make my program slower at runtime?
No. Rust uses a technique called monomorphization: at compile time it generates a specialized copy of your generic code for each concrete type that is actually used. So largest<i32> and largest<char> become two separate, fully optimized functions in the final binary. The result is that generic code runs exactly as fast as if you had written each version by hand — there is zero runtime cost for the abstraction. The trade-off is slightly larger compiled output and longer compile times.
Can a struct have more than one generic type parameter?
Yes. You can declare as many type parameters as you need, like struct Pair<T, U> { first: T, second: U }, which lets the two fields hold different types. Each parameter is independent and can have its own trait bounds. The standard library's Result<T, E> is a familiar example with two type parameters — one for the success value and one for the error. Use multiple parameters whenever the parts of your type genuinely vary independently.