Generics
Generics are fundamental for Rust.
Generic Structs
Structs can have type parameters.
struct Point<Precision> {
x: Precision,
y: Precision,
}
fn main() {
let point = Point { x: 1_u32, y: 2 };
let point: Point<i32> = Point { x: 1, y: 2 };
}
Note:
The part <Precision>
introduces a type parameter called Precision
. Often people just use T
but you don't have to!
Type Inference
- Inside a function, Rust can look at the types and infer the types of variables and type parameters.
- Rust will only look at other signatures, never other bodies.
- If the function signature differs from the body, the body is wrong.
Generic Enums
Enums can have type parameters.
enum Either<T, X> {
Left(T),
Right(X),
}
fn main() {
let alternative: Either<i32, f64> = Either::Left(123);
}
Note:
What happens if I leave out the <i32, f64>
specifier? What would type parameter X
be set to?
Generic Functions
Functions can have type parameters.
Note:
Default bounds are Sized
, so finding the size of the type is one thing that you can do. You can also take a reference or a pointer to the value.
Generic Implementations
struct Vector<T> {
x: T,
y: T,
}
impl<T> Vector<T> {
fn new(x: T, y: T) -> Vector<T> {
Vector { x, y }
}
}
impl Vector<f32> {
fn magnitude(&self) -> f32 {
((self.x * self.x) + (self.y * self.y)).sqrt()
}
}
fn main() {
let v1 = Vector::new(1.0, 1.0);
println!("{}", v1.magnitude());
let v2 = Vector::new(1, 1);
// println!("{}", v2.magnitude());
}
Note:
Can I call my_vector.magnitude()
if T is ... a String? A Person? A TCPStream?
Are there some trait bounds we could place on T
such that T + T -> T
and T * T -> T
and T::sqrt()
were all available?
The error:
error[E0599]: no method named `magnitude` found for struct `Vector<{integer}>` in the current scope
--> src/main.rs:22:23
|
1 | struct Vector<T> {
| ---------------- method `magnitude` not found for this struct
...
22 | println!("{}", v2.magnitude());
| ^^^^^^^^^ method not found in `Vector<{integer}>`
|
= note - the method was found for
- `Vector<f32>`
For more information about this error, try `rustc --explain E0599`.
Adding Bounds
- Generics aren't much use without bounds.
- A bound says which traits must be implemented on any type used for that type parameter
- You can apply the bounds on the type, or a function/method, or both.
Adding Bounds - Example
trait HasArea {
fn area(&self) -> f32;
}
fn print_area<T>(shape: &T) where T: HasArea {
let area = shape.area();
println!("Area = {area:?}");
}
struct UnitSquare;
impl HasArea for UnitSquare {
fn area(&self) -> f32 {
1.0
}
}
fn main() {
let u = UnitSquare;
print_area(&u);
}
Adding Bounds - Alt. Example
trait HasArea {
fn area(&self) -> f32;
}
fn print_area<T: HasArea>(shape: &T) {
let area = shape.area();
println!("Area = {area:?}");
}
struct UnitSquare;
impl HasArea for UnitSquare {
fn area(&self) -> f32 {
1.0
}
}
fn main() {
let u = UnitSquare;
print_area(&u);
}
Note:
This is exactly equivalent to the previous example, but shorter. However, if you end up with a large set of bounds, they are easier to format when at the end of the line.
General Rule
- If you can, try and avoid adding bounds to
structs
. - Simpler to only add them to the methods.
Multiple Bounds
You can specify multiple bounds.
trait HasArea {
fn area(&self) -> f32;
}
fn print_area<T: std::fmt::Debug + HasArea>(shape: &T) {
println!("Shape {:?} has area {}", shape, shape.area());
}
#[derive(Debug)]
struct UnitSquare;
impl HasArea for UnitSquare {
fn area(&self) -> f32 { 1.0 }
}
fn main() {
let u = UnitSquare;
print_area(&u);
}
impl Trait
- The
impl Trait
syntax in argument position was just syntactic sugar. - (It does something special in the return position though)
Note:
Some types that cannot be written out, like the closure, can be expressed as return types using impl
. e.g. fn score(y: i32) -> impl Fn(i32) -> i32
.
Caution
- Using Generics is Hard Mode Rust
- Don't reach for it in the first instance...
- Try and just use concrete types?
Generic over Constants
In Rust 1.51, we gained the ability to be generic over constant values too.
struct Polygon<const SIDES: u8> {
colour: u32
}
impl<const SIDES: u8> Polygon<SIDES> {
fn new(colour: u32) -> Polygon<SIDES> { Polygon { colour } }
fn print(&self) { println!("{} sides, colour=0x{:06x}", SIDES, self.colour); }
}
fn main() {
let triangle: Polygon<3> = Polygon::new(0x00FF00);
triangle.print();
}
Note:
SIDES
is a property of the type, and doesn't occupy any memory within any
values of that type at run-time - the constant is pasted in wherever it is used.
Generic Traits
Traits themselves can have type parameters too!
trait HasArea<T> {
fn area(&self) -> T;
}
// Here we only accept a shape where the `U` in `HasArea<Y>` is printable
fn print_area<T, U>(shape: &T) where T: HasArea<U>, U: std::fmt::Debug {
let area = shape.area();
println!("Area = {area:?}");
}
struct UnitSquare;
impl HasArea<f64> for UnitSquare {
fn area(&self) -> f64 {
1.0
}
}
fn main() {
let u = UnitSquare;
print_area(&u);
}
Special Bounds
- Some bounds apply automatically
- Special syntax to turn them off
Note:
This bound says "It must implement std::fmt::Debug, but I don't care if it has a size known at compile-time".
Things that don't have sizes known at compile time (but which may or may not implement std::fmt::Debug) include:
- String Slices
- Closures