Generics in Rust allow you to write flexible, reusable code that can work with any data type. They enable you to define functions, structs, enums, and methods that operate on generic types rather than specific types. This reduces code duplication and increases type safety.
Generic Functions
You can define functions that accept parameters of any type using generics. The generic type is specified in angle brackets (<T>)
.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
T
is a placeholder for any type.PartialOrd
is a trait that ensures the typeT
can be compared (required for the > operator).- This function works for any type that implements
PartialOrd
, such as integers, floats, or custom types.
Generic Structs
You can define structs that use generic types for their fields.
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 };
}
Point<T>
can hold values of any typeT
.- Both
x
andy
must be of the same typeT
.
If we want x
and y
to have different types.
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed_point = Point { x: 5, y: 4.0 };
}
Generic Enums
Enums can also use generics. A common example is Rust’s Option<T>
and Result<T, E>
enums.
enum Option<T> { // Option<T> can hold a value of type T (Some) or no value (None).
Some(T),
None,
}
enum Result<T, E> { // Result<T, E> can hold a success value of type T (Ok) or an error of type E (Err).
Ok(T),
Err(E),
}
Generic Methods
You can implement methods for structs or enums using generics.
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());
}
Trait Bounds
To restrict generics to types that implement specific behavior, you can use trait bounds.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
- T: PartialOrd ensures that T implements the PartialOrd trait, allowing comparison.
We can also use multiple trait bounds with the + syntax:
fn print_and_return<T: PartialOrd + std::fmt::Display>(value: T) -> T {
println!("Value: {}", value);
value
}
Where Clause
fn some_function<T, U>(t: T, u: U) -> i32
where
T: std::fmt::Display + Clone,
U: Clone + std::fmt::Debug,
{
println!("T: {}", t);
println!("U: {:?}", u);
42
}
Rust’s generics are zero-cost abstractions. The compiler generates specialized code for each concrete type used with generics, ensuring no runtime overhead.
For example, if you use Point<i32>
and Point<f64>
, the compiler generates two separate versions of the Point
struct and its methods, optimized for i32
and f64
.