Generics - Generic Types and Functions
Generics allow you to write code that works with multiple types while maintaining type safety. Instead of writing separate functions for each type, you can write one generic function that works with any type. Rust's generics have zero runtime cost thanks to monomorphization.
What Are Generics?
Generics are abstract stand-ins for concrete types. They let you write flexible, reusable code without sacrificing performance or type safety.
Generic Functions
Click Run to execute your code
Generic Type Parameters
Type parameters are specified in angle brackets <T>:
fn function_name(parameter: T) -> T {
// T is a placeholder for any type
}
Generic Structs
Structs can use generic type parameters to hold values of any type:
struct Point {
x: T,
y: T,
}
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
Multiple Type Parameters
You can use multiple type parameters:
struct Point {
x: T,
y: U,
}
let mixed = Point { x: 5, y: 4.0 }; // x is i32, y is f64
Generic Enums
Rust's most common enums are generic:
enum Option {
Some(T),
None,
}
enum Result {
Ok(T),
Err(E),
}
Generic Implementations
Click Run to execute your code
Implementation Types
| Type | Syntax | Use Case |
|---|---|---|
| Generic | impl<T> Point<T> |
Methods for all types |
| Specific | impl Point<f32> |
Methods for one type only |
| Constrained | impl<T: Display> Point<T> |
Methods for types with trait |
Combining Generics with Traits
Click Run to execute your code
Trait Bounds Syntax
Multiple ways to specify trait bounds:
// Inline syntax
fn notify(item: T) { }
// where clause (more readable for complex bounds)
fn notify(item: T)
where
T: Summary + Display,
{ }
// impl Trait syntax (simpler for parameters)
fn notify(item: impl Summary + Display) { }
Monomorphization
Rust compiles generic code into specific code for each type used:
// You write this:
fn largest(list: &[T]) -> &T { ... }
let numbers = vec![1, 2, 3];
let chars = vec!['a', 'b', 'c'];
largest(&numbers);
largest(&chars);
// Compiler generates this:
fn largest_i32(list: &[i32]) -> &i32 { ... }
fn largest_char(list: &[char]) -> &char { ... }
Blanket Implementations
Implement a trait for any type that satisfies trait bounds:
// Standard library does this for ToString
impl ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
// Now any type with Display automatically has to_string()
let num = 42;
let s = num.to_string(); // Works!
Best Practices
- Use descriptive type parameter names:
Tfor type,Efor error,K/Vfor key/value - Add trait bounds when needed: Constrain types to ensure required functionality
- Use where clauses: For complex bounds, improve readability
- Prefer generics over duplication: Write once, use with many types
- Don't over-generalize: Only make things generic when needed
- Combine with traits: Generics + traits = powerful abstractions
Common Mistakes
1. Forgetting trait bounds
// Wrong - T might not support comparison
fn largest(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { // Error: can't compare T
largest = item;
}
}
largest
}
// Correct - Add PartialOrd bound
fn largest(list: &[T]) -> &T {
// Now comparison works!
}
2. Mixing incompatible types in generic structs
// Wrong - x and y must be same type
struct Point {
x: T,
y: T,
}
let p = Point { x: 5, y: 4.0 }; // Error: i32 vs f64
// Correct - Use two type parameters
struct Point {
x: T,
y: U,
}
let p = Point { x: 5, y: 4.0 }; // OK!
3. Not specifying type parameters in impl blocks
// Wrong - Missing after impl
impl Point { // Error: can't find type T
fn x(&self) -> &T {
&self.x
}
}
// Correct - Declare type parameter
impl Point {
fn x(&self) -> &T {
&self.x
}
}
Exercise: Generics Practice
Task: Implement generic functions and a Container struct.
Click Run to execute your code
Show Solution
use std::fmt::Display;
fn find_max(list: &[T]) -> Option<&T> {
if list.is_empty() {
return None;
}
let mut max = &list[0];
for item in list {
if item > max {
max = item;
}
}
Some(max)
}
struct Container {
value: T,
}
impl Container {
fn new(value: T) -> Self {
Self { value }
}
fn get(&self) -> &T {
&self.value
}
fn set(&mut self, value: T) {
self.value = value;
}
}
impl Container {
fn print(&self) {
println!("Container holds: {}", self.value);
}
}
fn swap(a: &mut T, b: &mut T) {
std::mem::swap(a, b);
}
Summary
- Generics enable code reuse across multiple types
- Type parameters are specified in angle brackets
<T> - Work with functions, structs, enums, and implementations
- Trait bounds constrain generic types
- Multiple type parameters with
<T, U, V> - where clauses for complex bounds
- Monomorphization creates specific code at compile time
- Zero runtime cost - same performance as non-generic code
- Blanket implementations apply traits to many types
- Combine with traits for powerful abstractions
What's Next?
Now that you understand generics and traits, you're ready to explore more advanced topics like Closures (anonymous functions) and Iterators (lazy evaluation). These features work together with generics and traits to enable functional programming patterns in Rust.
Enjoying these tutorials?