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.
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());
}// 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"));
}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.