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 leastDebug.
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
Debugvariant later and everymatchonLevelfails to compile until handled. Refactoring with a net. - Expressions:
matchproduces a value (noreturnneeded 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
- Add a
summary_text(&[LogEntry]) -> Stringfunction building"2 info, 1 warn, 1 error"— with a unit test. - Introduce
enum Priority { Low, High }and addfn priority(&Level) -> Priority(errors are High). Use it to filter before printing. - Replace
counts_by_level’smatchwith 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.