Both the collection types and the Option and Result enums explained earlier in this course would be nowhere near useful or practical if they couldn't work with as many types as possible. Vectors and hash maps can act as collections for many different data types, from primitive integers to complicated structs, and Options and Results can be defined in a similar way.
This is made possible through generic types - ambiguous representations of multiple possible types, that allow functions, methods, enums and structs to handle parameters or fields, respectively, regardless of their actual data type:
fn first_of_vec<T: Copy>(v: Vec<T>) -> T {
v[0] // of type T, where T is copyable
}
fn main() {
println!(
"{} {} {}",
first_of_vec(vec![1, 2, 3]),
first_of_vec(vec!["a", "b", "c"]),
first_of_vec(vec![true, false, false])
)
}
Note how the first_of_vec() function in the above example uses the angle bracket (< >) syntax previously seen for definitions of vectors, hash maps, and other built-in structs and enums in the standard Rust library.
This is exactly how these other built-in features can behave in this manner: rather than exclusively take and return values of specific types, they are defined to take any type, as long as that type implements the required traits:
// The actual library definition of the Result enum:
pub enum Result<T, E> {
Ok(T),
Err(E),
}
Result takes two completely generic, non-constrained types T and E. All that the compiler can infer about what these two types are is that they can potentially be different types from each other.
The specific definition of first_of_vec() with T: Copy in the first example essentially means that first_of_vec() can take a vector containing elements of any one type, as long as that type implements the Copy trait (meaning it can be copied in memory instead of needing to be borrowed and referenced).
If the first_of_vec() example was implemented without the specification that values of type T are copyable (that is, if the function was defined as first_of_vec<T>() rather than first_of_vec<T: Copy>()), the code would fail to compile because the compiler can't infer enough about the parameter's typing or traits to know if its elements can be cloned or copied:
error[E0507]: cannot move out of index of `Vec<T>`
--> src/main.rs:2:5
|
2 | v[0]
| ^^^^ move occurs because value has type `T`, which does not implement the `Copy` trait
|
help: if `T` implemented `Clone`, you could clone the value
--> src/main.rs:1:17
|
1 | fn first_of_vec<T>(v: Vec<T>) -> T {
| ^ consider constraining this type parameter with `Clone`
2 | v[0] // of type T, where T is copyable
| ---- you could clone this value
One valid alternative to the syntax in the first example, however, is to introduce the trait binding at the end of the function typing (after the parameters and return type) with the where keyword:
fn first_of_vec<T>(v: Vec<T>) -> T where T: Copy {
v[0]
}
fn main() {
println!(
"{} {} {}",
first_of_vec(vec![1, 2, 3]),
first_of_vec(vec!["a", "b", "c"]),
first_of_vec(vec![true, false, false])
)
}
Exercise:
Fix the following code by adding the missing trait binding(s):
Hint: Pay attention to the error message. Multiple trait bindings for one generic are separated with the addition symbol (+).
Traits are very important to work with when implementing generics, and how they define behaviour will be explained in more detail on the next page.