What you'll learn: Generic type parameters, monomorphization (zero-cost generics), trait bounds, and how Rust generics compare to C++ templates — with better error messages and no SFINAE.
<>, e.g.: <T>. The parameter can have any legal identifier name, but is typically kept short for brevityT that is encountered// Returns a tuple of type <T> composed of left and right of type <T>
fn pick<T>(x: u32, left: T, right: T) -> (T, T) {
if x == 42 {
(left, right)
} else {
(right, left)
}
}
fn main() {
let a = pick(42, true, false);
let b = pick(42, "hello", "world");
println!("{a:?}, {b:?}");
}
<T> (example: f32 vs. u32)#[derive(Debug)] // We will discuss this later
struct Point<T> {
x : T,
y : T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point {x, y}
}
fn set_x(&mut self, x: T) {
self.x = x;
}
fn set_y(&mut self, y: T) {
self.y = y;
}
}
impl Point<f32> {
fn is_secret(&self) -> bool {
self.x == 42.0
}
}
fn main() {
let mut p = Point::new(2, 4); // i32
let q = Point::new(2.0, 4.0); // f32
p.set_x(42);
p.set_y(43);
println!("{p:?} {q:?} {}", q.is_secret());
}
🟢 Starter
Point type to use two different types (T and U) for x and y#[derive(Debug)]
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn new(x: T, y: U) -> Self {
Point { x, y }
}
}
fn main() {
let p1 = Point::new(42, 3.14); // Point<i32, f64>
let p2 = Point::new("hello", true); // Point<&str, bool>
let p3 = Point::new(1u8, 1000u64); // Point<u8, u64>
println!("{p1:?}");
println!("{p2:?}");
println!("{p3:?}");
}
// Output:
// Point { x: 42, y: 3.14 }
// Point { x: "hello", y: true }
// Point { x: 1, y: 1000 }
: after the generic type parameter, or using where. The following defines a generic function get_area that takes any type T as long as it implements the ComputeArea trait trait ComputeArea {
fn area(&self) -> u64;
}
fn get_area<T: ComputeArea>(t: &T) -> u64 {
t.area()
}
trait Fish {}
trait Mammal {}
struct Shark;
struct Whale;
impl Fish for Shark {}
impl Fish for Whale {}
impl Mammal for Whale {}
fn only_fish_and_mammals<T: Fish + Mammal>(_t: &T) {}
fn main() {
let w = Whale {};
only_fish_and_mammals(&w);
let _s = Shark {};
// Won't compile
only_fish_and_mammals(&_s);
}
PrintDescription trait and a generic struct Shape with a member constrained by the traittrait PrintDescription {
fn print_description(&self);
}
struct Shape<S: PrintDescription> {
shape: S,
}
// Generic Shape implementation for any type that implements PrintDescription
impl<S: PrintDescription> Shape<S> {
fn print(&self) {
self.shape.print_description();
}
}
🟡 Intermediate
struct with a generic member cipher that implements CipherTexttrait CipherText {
fn encrypt(&self);
}
// TO DO
//struct Cipher<>
encrypt on the struct impl that invokes encrypt on cipher// TO DO
impl for Cipher<> {}
CipherText on two structs called CipherOne and CipherTwo (just println() is fine). Create CipherOne and CipherTwo, and use Cipher to invoke themtrait CipherText {
fn encrypt(&self);
}
struct Cipher<T: CipherText> {
cipher: T,
}
impl<T: CipherText> Cipher<T> {
fn encrypt(&self) {
self.cipher.encrypt();
}
}
struct CipherOne;
struct CipherTwo;
impl CipherText for CipherOne {
fn encrypt(&self) {
println!("CipherOne encryption applied");
}
}
impl CipherText for CipherTwo {
fn encrypt(&self) {
println!("CipherTwo encryption applied");
}
}
fn main() {
let c1 = Cipher { cipher: CipherOne };
let c2 = Cipher { cipher: CipherTwo };
c1.encrypt();
c2.encrypt();
}
// Output:
// CipherOne encryption applied
// CipherTwo encryption applied
Rust types can be used to enforce state machine transitions at compile time
Drone with say two states: Idle and Flying. In the Idle state, the only permitted method is takeoff(). In the Flying state, we permit land()One approach is to model the state machine using something like the following
enum DroneState {
Idle,
Flying
}
struct Drone {x: u64, y: u64, z: u64, state: DroneState} // x, y, z are coordinates
PhantomData<T>PhantomData<T> is a zero-sized marker data type. In this case, we use it to represent the Idle and Flying states, but it has zero runtime sizetakeoff and land methods take self as a parameter. This is referred to as consuming (contrast with &self which uses borrowing). Basically, once we call the takeoff() on Drone<Idle>, we can only get back a Drone<Flying> and viceversastruct Drone<T> {x: u64, y: u64, z: u64, state: PhantomData<T> }
impl Drone<Idle> {
fn takeoff(self) -> Drone<Flying> {...}
}
impl Drone<Flying> {
fn land(self) -> Drone<Idle> { ...}
}
- [▶ Try it in the Rust Playground](https://play.rust-lang.org/)
T with PhantomData<T> (zero-size)impl State<T>self to transition from one state to anotherzero cost abstractions. The compiler can enforce the state machine at compile time and it's impossible to call methods unless the state is rightself can be useful for builder patterns#[derive(default)]
enum PinState {
#[default]
Low,
High,
}
#[derive(default)]
struct GPIOConfig {
pin0: PinState,
pin1: PinState
...
}