Rust Generics
Generics let one function work for many types. A generic function names a placeholder, a letter in angle brackets, `<T>`, instead of fixing one type like i32.
The same function written twice
largest_i32 and largest_char share one body. Only the type differs.
fn largest_i32(list: &[i32]) -> &i32 {
let mut best = &list[0];
for item in list { if item > best { best = item; } }
best
}
fn largest_char(list: &[char]) -> &char {
let mut best = &list[0];
for item in list { if item > best { best = item; } }
best
}
println!("{}", largest_i32(&[3, 9, 4]));
println!("{}", largest_char(&['a', 'z', 'm']));One placeholder for the type
Write the type as a letter in angle brackets. The bound after the letter says what it must be able to do.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
// one body, written for the placeholder type T
}The same function, two types
Call it with numbers, then with chars. You wrote the function once.
println!("{}", largest(&[3, 9, 4]));
println!("{}", largest(&['a', 'z', 'm']));Structs can be generic too
Both fields share the one letter, so both must hold the same type.
struct Point<T> {
x: T,
y: T,
}
let p = Point { x: 3, y: 4 };
println!("({}, {})", p.x, p.y);Two letters allow mixed types
Give the fields different letters and they may hold different types.
struct Point<T, U> {
x: T,
y: U,
}
let mixed = Point { x: 5, y: 10.4 };
println!("{} and {}", mixed.x, mixed.y);The two generic enums
Option<T> holds a T or nothing. Result<T, E> holds a T or an error E.
let maybe: Option<i32> = Some(7);
let outcome: Result<i32, &str> = Ok(7);
println!("{:?} {:?}", maybe, outcome);Name the type with the turbofish
When Rust cannot work out T from the call, name it. The ::<...> part is nicknamed the turbofish.
fn wrap<T>(item: T) -> Vec<T> {
vec![item]
}
println!("{:?}", wrap::<char>('a'));Example
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut best = &list[0];
for item in list { if item > best { best = item; } }
best
}
fn main() {
println!("{}", largest(&[3, 9, 4]));
println!("{}", largest(&['a', 'z', 'm']));
}Key points
- Arrays use a compile-time size as a parameter too: [T; N]
- One generic definition names a placeholder type and serves many real types.
- Many generic letters are allowed.
- More than a few harms readability.
- Treat that as a sign to split the type into smaller pieces.
- Declaring the letter is what makes a thing generic.
- A function that merely uses a concrete Bag<i32> is not generic at all.
- That holds however generic Bag itself is.
- Only a <T> declared at the first mention counts.
- A generic body is fully checked when it is defined.
- The check uses only its bounds.
- If the body calls a method, a bound must promise it.
- The check does not wait until some call site picks a real type.
- Rust builds a specialised copy of the code for each real type you use.
- This is called monomorphisation, and it costs nothing at run time.
- Rust fills in the real type at each call.
In this example, largest works on a slice of any comparable type T. It starts with best pointing at the first item. Then it walks the list, and whenever an item is bigger than best, best moves to that item. main calls it twice. With a list of numbers it prints 9. With a list of chars it prints z. The compiler actually built two versions of the function, one per type — you just wrote it once. Rust Monomorphisation shows what the compiler does with these copies.
Note
Generic does not mean slow or dynamic.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
What is monomorphisation?