Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Structs, Enums, and match

Goal: you can model a small domain (log levels, entries, summaries) with structs and enums, and dispatch with match — no inheritance, no exceptions.

Structs: data with named fields

#![allow(unused)]
fn main() {
struct LogEntry {
    ts: String,
    level: Level,
    msg: String,
}
}
  • Construct: LogEntry { ts, level, msg } (field-init shorthand when names match — clippy enforces it).
  • Methods live in a separate impl LogEntry { ... } block — data and behavior are textually apart, unlike OO class bodies.
  • #[derive(Debug, PartialEq, Eq)] auto-implements printing and comparison — most structs in our services derive at least Debug.

Enums: closed sets of variants

Rust enums are sum types — each variant can carry its own data:

#![allow(unused)]
fn main() {
enum Level { Info, Warn, Error }        // simple, C-like — but type-checked

enum LineOutcome {                       // variants carry different payloads
    Ok(LogEntry),
    Skipped { reason: &'static str },
    Empty,
}
}

That second shape is exactly how std defines Option<T> and Result<T, E> — and how our services define error enums (M2). One type, finite variants, compiler-checked handling everywhere.

match: the only dispatch you need

#![allow(unused)]
fn main() {
impl Level {
    fn as_str(&self) -> &'static str {
        match self {
            Level::Info => "INFO",
            Level::Warn => "WARN",
            Level::Error => "ERROR",
        }
    }
}
}
  • Exhaustive: add a Debug variant later and every match on Level fails to compile until handled. Refactoring with a net.
  • Expressions: match produces a value (no return needed per arm).
  • Patterns do the unwrapping: Some(x) =>, Ok(v) =>, Err(e) => — destructuring built into the language.

Counting with an array instead of a HashMap — allocation-free and total:

#![allow(unused)]
fn main() {
fn counts_by_level(entries: &[LogEntry]) -> [usize; 3] {
    let mut counts = [0usize; 3];
    for e in entries {
        match e.level {
            Level::Info => counts[0] += 1,
            Level::Warn => counts[1] += 1,
            Level::Error => counts[2] += 1,
        }
    }
    counts
}
}

(For large or open-ended level sets you would reach for HashMap<Level, usize> — the lab keeps it deliberately small.)

Exercise

  1. Add a summary_text(&[LogEntry]) -> String function building "2 info, 1 warn, 1 error" — with a unit test.
  2. Introduce enum Priority { Low, High } and add fn priority(&Level) -> Priority (errors are High). Use it to filter before printing.
  3. Replace counts_by_level’s match with an index lookup (match e.level { l => counts[l as usize] += 1 } after adding #[repr(usize)]-style discriminants). Which version reads better? Which survives adding a variant?

Done when: tests green for summary_text, and you can articulate why exhaustive match beats a default arm for domain enums.