Step 3 of 6
Generic structs and enums
Types can have type parameters too. A point on a game grid uses whole numbers; a GPS position uses decimals. One definition covers both:
struct Point<T> {
x: T,
y: T,
}
struct Labeled<T> {
label: String,
value: T,
}
#[derive(Debug)]
enum Reading<T> {
Value(T),
OutOfRange,
Missing,
}
fn main() {
let tile = Point { x: 3, y: 7 };
let gps = Point { x: 52.52, y: 13.40 };
println!("tile at ({}, {}), gps at ({}, {})", tile.x, tile.y, gps.x, gps.y);
let temp = Labeled { label: String::from("kitchen"), value: 21.5 };
let door = Labeled { label: String::from("front door open"), value: false };
println!("{}: {}", temp.label, temp.value);
println!("{}: {}", door.label, door.value);
let readings: [Reading<u32>; 3] = [Reading::Value(19), Reading::OutOfRange, Reading::Missing];
for r in &readings {
match r {
Reading::Value(v) => println!("{v} degrees"),
other => println!("no value: {other:?}"),
}
}
}
tile at (3, 7), gps at (52.52, 13.4)
kitchen: 21.5
front door open: false
19 degrees
no value: OutOfRange
no value: Missing
struct Point<T>declares the parameter; the fields use it.tileis aPoint<i32>andgpsis aPoint<f64>. They're two different types, built from one definition.Labeled<T>mixes a fixed type (labelis always aString) with a generic one:tempis aLabeled<f64>,dooraLabeled<bool>.- To write the full type, put the parameter in angle brackets:
let origin: Point<f64> = Point { x: 0.0, y: 0.0 };, or[Reading<u32>; 3]above. - If two fields may have different types, use two parameters:
struct Pair<K, V> { key: K, value: V }.
Option and Result are generic enums
Reading<T> is your own generic enum, and it follows the same pattern as the two most important enums in Rust:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Nothing magic: Option<String> and Result<u32, ParseIntError> are these definitions with the parameters filled in.
Common mistakes
- Mixing types in fields that share one parameter. In
Point<T>,xandyare bothT:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let p = Point { x: 5, y: 4.0 };
println!("{} {}", p.x, p.y);
}
error[E0308]: mismatched types
| let p = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
- A variant with no data, and no other hint.
let r = Reading::Missing;alone givestype annotations needed for `Reading<_>`: a missing reading of what? Write the type,let r: Reading<u32> = Reading::Missing;. The same happens withNoneandVec::new(). - Leaving out the parameter in a signature.
fn show(p: &Point)givesmissing generics for struct `Point`. Write&Point<i32>for one specific type, or make the function generic:fn show<T: Display>(p: &Point<T>).
Your task
Fill in the blanks: declare the type parameter of Pair, use it for second, and make total accept only pairs of i32. The program prints:
90 vs 75
ana vs bo
total 165
Challenge 2
A sensor log is a slice of Reading<T> (in the starter). Write fn first_value<T: Copy>(readings: &[Reading<T>]) -> Option<T> that returns the first Value in the slice, skipping Missing entries, or None if there's no value at all.
Challenge 3
From scratch: define a generic struct Labeled<T> with the fields label: String and value: T, then write fn label_all<T>(label: &str, values: Vec<T>) -> Vec<Labeled<T>>, which labels each value <label> #1, <label> #2, and so on. label_all("sensor", vec![21.5, 19.0]) gives two items labeled sensor #1 and sensor #2.
Previous: Trait bounds: what T is allowed to do Next: Methods on generic types: impl<T>