Generics are a powerful feature in Rust that allow you to write flexible, reusable code without sacrificing type safety. They enable you to define functions, structs, enums, and methods that work with multiple types while still being checked at compile time.
Why Use Generics?
Generics help you:
- Write code that works with many different types.
- Avoid code duplication.
- Maintain type safety and performance (no runtime overhead).
Generic Functions
You can define functions that work with any type by using generic type parameters.
// A function that finds the largest value in a slice
fn largest<T: PartialOrd>(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);
}
In this example, T: PartialOrd is a trait bound that specifies T must implement the PartialOrd trait (which allows comparison).
Generic Structs
You can define structs that use generic type parameters.
// A Point struct that can hold coordinates of any type
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 };
}
Multiple Generic Type Parameters
You can use multiple generic types in a single struct.
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed_point = Point { x: 5, y: 4.0 }; // x is i32, y is f64
}
Generic Enums
Rust’s Option and Result enums are examples of generic enums in the standard library.
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Generic Methods
You can define methods on structs with generic type parameters.
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// Method only for Point<f32>
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
let p2 = Point { x: 3.0, y: 4.0 };
println!("Distance from origin: {}", p2.distance_from_origin());
}
Trait Bounds
Trait bounds specify that a generic type must implement certain traits. This allows you to use methods from those traits on generic types.
use std::fmt::Display;
// T must implement the Display trait
fn print_item<T: Display>(item: T) {
println!("Item: {}", item);
}
// Multiple trait bounds
fn notify<T: Display + Clone>(item: T) {
println!("Breaking news! {}", item);
}
// Alternative syntax with `where` clause (more readable for complex bounds)
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// function body
0
}
Performance
One of Rust’s key strengths is that generics have zero runtime cost. Rust achieves this through monomorphization: at compile time, Rust generates specialized versions of generic code for each concrete type used. This means generic code is just as fast as if you had written separate functions for each type manually.
Summary
Generics in Rust provide:
- Code reusability across multiple types
- Type safety at compile time
- Zero runtime overhead
- Flexibility with trait bounds
They are essential for writing idiomatic, efficient Rust code and are used extensively throughout the standard library.