Rust Generics Tutorial
In this Rust tutorial we learn about how Generics let us define placeholder types for structs, enums, methods etc.
We also take a look at how to implement certain traits, and combinations of traits, with the type parameter.
What are Generics
Generics allow us to define placeholder types for our methods, functions, structs, enums, collections and traits.
As an example, let’s consider a HashMap Collection . A HashMap has two generic types, one for the keys, and one for the values.
HashMap<key_type, value_type>
// or
HashMap<T, U>
We’re not limited to, for example, integers for the keys and strings for the values. We can have any types we want.
use std::collections::HashMap;
fn main() {
let mut state_codes: HashMap<&str, &str> = HashMap::new();
state_codes.insert("NV", "Nevada");
state_codes.insert("NY", "New York");
let mut numbers: HashMap<i32, &str> = HashMap::new();
numbers.insert(1, "One");
numbers.insert(2, "Two");
}
We don’t need a different key/value collection for every single type combination, we can just use one collection and specify whichever type we want.
We write less code to achieve the same result, and we get type safety as a bonus.
The <T> type parameter
The
In a generic definition, we write the type parameter between open and close angle brackets after the name.
fn function_name<T>()
In the syntax example above, we can see that the type parameter is defined after the function name, between open and close angle brackets. The same rule applies to structs, enums etc.
To define multiple generic types, we simply separate them with commas.
fn function_name<T, U>()
In the example above, we use the letter U as the second generic type. This is simply a convention to continue down the alphabet after T.
Generic Struct
To define a generic struct, we write the type parameter after the struct name, between open and close angle brackets.
struct StructName<T, U, ...> {
// struct body
}
struct Foo<T, U> {
a: T,
b: U
}
fn main() {
let x = Foo {
a : "Hello",
b : 3.14
};
println!("x.a = {}", x.a);
println!("x.b = {}", x.b);
}
In the example above, our struct has two generic types, T and U. Inside the struct we specify that we want ‘a’ to be of type T, and ‘b’ to be of type U.
When we assign values to the struct’s parameters in the main() function, the Rust compiler will automatically infer the type from those values. x.a will be of type string, and x.b will be of type float.
Generic function/method
Generic functions allow some or all of their arguments to be parameterised with generic types.
fn function_name<T> (param_name: T) -> T {
// function body
}
Unfortunately, generic functions aren’t always as straightforward as we would think.
As an example, let’s consider a simple function that prints a string to the console.
fn console_log<T> (x: T) {
println!("{}", x);
}
fn main() {
console_log("Hello World");
}
We would expect the example above to work, but the compiler raises an error instead.
error[E0277]: `T` doesn't implement `std::fmt::Display`
--> src\main.rs:3:17
|
3 | println!("{}", x);
| ^ `T` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `T`
In this case, the parameter’s type (T) must implement the Display trait from the standard library.
use std::fmt::Display;
fn console_log<T: Display> (x: T) {
println!("{}", x);
}
fn main() {
console_log("Hello World");
}
This time, our message is printed to the console.
As another example, let’s consider a function this time that multiplies a number by itself.
fn square<T> (x: T) -> T {
return x * x;
}
fn main() {
let result = square(5);
println!("Result = {:?}", result);
}
Again, the compiler raises an error.
error[E0369]: binary operation `*` cannot be applied to type `T`
--> src\main.rs:3:11
|
3 | return x * x;
| - ^ - T
| |
| T
|
= note: `T` might need a bound for `std::ops::Mul`
According to the error, we need to implement the Mul trait for T.
Mul has the output associated type, which is the resulting type after we use the * operator. So, let’s do the easiest thing and say that Output (T * T) should = T.
Note that Output is written inside its own set of angle brackets.
use std::ops::Mul;
fn square<T: Mul<Output = T>> (x: T) -> T {
return x * x;
}
fn main() {
let result = square(5);
println!("Result = {:?}", result);
}
But this gives us a new error.
error[E0382]: use of moved value: `x`
--> src\main.rs:5:13
|
3 | fn square<T: Mul<Output = T>> (x: T) -> T {
| - - move occurs because `x` has type `T`, which does not implement the `Copy` trait
| |
| consider adding a `Copy` constraint to this type argument
4 |
5 | return x * x;
| - ^ value used here after move
| |
| value moved here
This means we should implement the Copy trait as well, to duplicate the value instead of moving it. So we simply write + Copy after Mul to implement it.
use std::ops::Mul;
fn square<T: Mul<Output = T> + Copy> (x: T) -> T {
return x * x;
}
fn main() {
let result = square(5);
println!("Result = {:?}", result);
}
Now our example finally works and prints the correct result.
Summary: Points to remember
- Generics allow us to create placeholder types.
- The type parameter is typically expressed as <T> and can be any type.
- We can create generic methods, functions, structs, enums, collections and traits.
- Generic functions/methods can generalize both their parameters and return type.
- We can implement traits Like Copy and Display on the type parameter(s).