#15 Generics
<T>, where, bounds
Generic functions
Generics let you write code that works with many types without duplication. You declare a type parameter in angle brackets after the function name:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in &list[1..] {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers));
let chars = vec!['a', 'z', 'm', 'b'];
println!("Largest char: {}", largest(&chars));
}The compiler generates specialized versions for each type you use -- this is called monomorphization. There is no runtime cost.
Generic structs
Structs can also be generic. This lets you create data structures that work with any type:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
fn main() {
let int_point = Point::new(5, 10);
let float_point = Point::new(1.5, 3.7);
println!("Int: ({}, {})", int_point.x, int_point.y);
println!("Float: ({}, {})", float_point.x, float_point.y);
}You can use multiple type parameters to allow different types for each field:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 10.5 };
println!("Point {{ x: {}, y: {} }}", mixed.x, mixed.y);
}Generic enums
You have already used generic enums: Option<T> and Result<T, E> are both generic! Here is how you define your own:
enum Either<L, R> {
Left(L),
Right(R),
}
fn divide(a: f64, b: f64) -> Either<f64, String> {
if b == 0.0 {
Either::Right(String::from("Cannot divide by zero"))
} else {
Either::Left(a / b)
}
}
fn main() {
match divide(10.0, 3.0) {
Either::Left(result) => println!("Result: {:.2}", result),
Either::Right(err) => println!("Error: {}", err),
}
}Trait bounds with generics
Often you need the generic type to have certain capabilities. You specify this with trait bounds:
use std::fmt::Display;
// T must implement Display so we can print it
fn print_value<T: Display>(value: T) {
println!("Value: {}", value);
}
// Multiple bounds with +
fn print_and_clone<T: Display + Clone>(value: T) {
let cloned = value.clone();
println!("Original: {}, Clone: {}", value, cloned);
}
fn main() {
print_value(42);
print_value("hello");
print_and_clone(String::from("Rust"));
}The where clause
When trait bounds get complex, the where clause makes signatures more readable:
use std::fmt::{Display, Debug};
// Without where (hard to read)
fn complex<T: Display + Clone, U: Debug + PartialOrd>(t: T, u: U) {
println!("{}, {:?}", t, u);
}
// With where (much cleaner)
fn complex_clean<T, U>(t: T, u: U)
where
T: Display + Clone,
U: Debug + PartialOrd,
{
println!("{}, {:?}", t, u);
}You can also use where clauses on impl blocks:
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T>
where
T: Display,
{
fn show(&self) {
println!("{}", self.value);
}
}
impl<T> Wrapper<T>
where
T: Display + Into<f64> + Copy,
{
fn distance_from_origin(&self) -> f64 {
let val: f64 = self.value.into();
val.abs()
}
}
fn main() {
let w = Wrapper { value: 5.0_f64 };
w.show();
println!("Distance from origin: {:.2}", w.distance_from_origin());
}Summary
fn name<T>(x: T) // Generic function
struct Name<T> { field: T } // Generic struct
enum Name<T> { Variant(T) } // Generic enum
<T: Trait> // Trait bound
<T: Trait1 + Trait2> // Multiple bounds
where T: Trait1 + Trait2 // where clause
impl<T> Name<T> { } // Generic impl
impl<T: Trait> Name<T> { } // Conditional implYour turn
Try running the generics demo with cargo run and verify with cargo check: