Sometimes, when writing a function or data type, we may want it to work for multiple types of arguments. In Rust, we can do this with generics. Generics are called ‘parametric polymorphism’ in type theory, which means that they are types or functions that have multiple forms (‘poly’ is multiple, ‘morph’ is form) over a given parameter (‘parametric’).
Anyway, enough with type theory, let’s check out some generic code. Rust’s
standard library provides a type, Option<T>, that’s generic:
enum Option<T> { Some(T), None, }
The <T>`part, which you’ve seen a few times before, indicates that this is a generic data type. Inside the declaration of our enum, wherever we see aT, we substitute that type for the same type used in the generic. Here’s an example of usingOption
let x: Option<i32> = Some(5);
In the type declaration, we say Option<i32>. Note how similar this looks to
Option<T>. So, in this particular Option`Option,`, T`Thas the value ofi32. On the right-hand side of the binding, we do make aSome(T), where`, where T`Tis` is 5`5. Since that’s ani32`, the two sides match, and Rust is happy. If they didn’t
match, we’d get an error:
let x: Option<f64> = Some(5); // error: mismatched types: expected `core::option::Option<f64>`, // found `core::option::Option<_>` (expected f64 but found integral variable)
That doesn’t mean we can’t make Option<T>s that hold an f64`f64`! They just have
to match up:
let x: Option<i32> = Some(5); let y: Option<f64> = Some(5.0f64);
This is just fine. One definition, multiple uses.
Generics don’t have to only be generic over one type. Consider another type from Rust’s standard library that’s similar, Result<T, E>:
enum Result<T, E> { Ok(T), Err(E), }
This type is generic over two types: T`Tand` and E`E. By the way, the capital letters can be any letter you’d like. We could defineResult
enum Result<A, Z> { Ok(A), Err(Z), }
if we wanted to. Convention says that the first generic parameter should be
T`T, for ‘type’, and that we useE` for ‘error’. Rust doesn’t care, however.
The Result<T, E> type is intended to be used to return the result of a
computation, and to have the ability to return an error if it didn’t work out.
We can write functions that take generic types with a similar syntax:
fn main() { fn takes_anything<T>(x: T) { // do something with x } }fn takes_anything<T>(x: T) { // do something with x }
The syntax has two parts: the <T>`says “this function is generic over one type,T”, and thex: Tsays “x has the typeT`.”
Multiple arguments can have the same generic type:
fn main() { fn takes_two_of_the_same_things<T>(x: T, y: T) { // ... } }fn takes_two_of_the_same_things<T>(x: T, y: T) { // ... }
We could write a version that takes multiple types:
fn main() { fn takes_two_things<T, U>(x: T, y: U) { // ... } }fn takes_two_things<T, U>(x: T, y: U) { // ... }
Generic functions are most useful with ‘trait bounds’, which we’ll cover in the section on traits.
You can store a generic type in a struct`struct` as well:
struct Point<T> { x: T, y: T, } let int_origin = Point { x: 0, y: 0 }; let float_origin = Point { x: 0.0, y: 0.0 };
Similarly to functions, the <T>`is where we declare the generic parameters, and we then usex: T` in the type declaration, too.