Lesson 9 of 12

Generics & Lifetimes

Learn how let one serve many types, and how lifetimes tell the that a will not outlive the data it points to.

Rust
use std::fmt::Display;

// Generic function with trait bounds
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
    let mut best = items[0];
    for &item in items {
        if item > best { best = item; }
    }
    best
}

// where clauses keep complicated signatures readable
fn describe<T, U>(a: T, b: U) -> String
where
    T: Display + Clone,
    U: Display,
{
    format!("{a} and {b}")
}

// Generic struct with methods
struct Pair<T> { left: T, right: T }

impl<T: PartialOrd + Display> Pair<T> {
    fn new(left: T, right: T) -> Self { Self { left, right } }

    fn larger(&self) -> &T {
        if self.left > self.right { &self.left } else { &self.right }
    }
}

fn main() {
    println!("{}", largest(&[1, 5, 3]));
    println!("{}", largest(&[1.2, 0.4]));
    println!("{}", describe("x", 42));
    println!("{}", Pair::new(3, 9).larger());
}
Rust
// A lifetime annotation does NOT change how long anything lives.
// It tells the compiler how the lifetimes of inputs and outputs RELATE.

// "The returned reference lives as long as the shorter of a and b."
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

// Without the annotation the compiler cannot know which input the result
// borrows from, and refuses to compile.

// Structs holding references need a lifetime parameter
struct Parser<'a> {
    input: &'a str,
    position: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Self { Parser { input, position: 0 } }

    fn next_word(&mut self) -> Option<&'a str> {
        let rest = &self.input[self.position..];
        let trimmed = rest.trim_start();
        if trimmed.is_empty() { return None; }
        let skipped = rest.len() - trimmed.len();
        let end = trimmed.find(' ').unwrap_or(trimmed.len());
        self.position += skipped + end;
        Some(&trimmed[..end])
    }
}

fn main() {
    let text = String::from("hello brave new world");
    let mut parser = Parser::new(&text);
    while let Some(word) = parser.next_word() {
        println!("{word}");
    }
    println!("{}", longest("short", "much longer"));
}
Rust
use std::fmt;

// Traits define shared behaviour; impl blocks provide it.
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String { "shape".to_string() }   // default method
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
    fn name(&self) -> String { "circle".into() }
}
impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.side }
}

// Static dispatch: one specialized copy per type, no runtime cost
fn print_area(shape: &impl Shape) { println!("{}: {:.2}", shape.name(), shape.area()); }

// Dynamic dispatch: one function, a vtable lookup at runtime — needed for mixed collections
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

// Implementing standard traits unlocks language features
impl fmt::Display for Circle {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Circle(r={})", self.radius)
    }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];
    print_area(&Circle { radius: 2.0 });
    println!("{:.2}", total_area(&shapes));
    println!("{}", Circle { radius: 3.0 });
}
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.