Structs, Enums & Pattern Matching
Module Overview
Section titled “Module Overview”This module introduces three of Rust’s most important building blocks for modeling data and control flow.
Structs are named collections of fields. They let you group related values under a single type, attach methods via impl blocks, and derive useful traits like Debug and Clone.
Enums model choices. Each variant can optionally carry different data, making them far more expressive than the C-style enums you may know from other languages. Rust’s Option<T> and Result<T, E> are both built-in enums.
match is Rust’s pattern-matching expression. It branches on the shape of a value, binds data from enum variants, and is checked exhaustively by the compiler — every possible case must be handled.
Together, these three features let you define precise, self-documenting data models and write control flow that the compiler verifies for you.
flowchart TD
Q{What are you modeling?} -->|A record with named fields, all present at once| S[struct]
Q -->|One of several distinct choices, maybe with different data each| E[enum]
Q -->|You already have an enum or Option/Result value| M["match / if let"]
S -->|"add behavior"| I["impl block: methods + Self"]
E -->|"branch on which variant it is"| M
M -->|"compiler checks every variant is handled"| Ex["exhaustiveness"] #[derive(Debug)]struct Point { x: f64, y: f64 }
#[derive(Debug)]enum Direction { North, South, East, West }
fn main() { let p = Point { x: 3.0, y: 4.0 }; println!("Point: ({}, {})", p.x, p.y); println!("Debug: {:?}", p);
let directions = [ Direction::North, Direction::South, Direction::East, Direction::West, ];
for dir in &directions { let label = match dir { Direction::North => "North", Direction::South => "South", Direction::East => "East", Direction::West => "West", }; println!("Heading {}", label); }}Compiling…
What the Compiler Says: Custom Types Have No Default Way to Print
Section titled “What the Compiler Says: Custom Types Have No Default Way to Print”A struct or enum you define has no built-in human-readable format. println!("{}", value) asks for the Display trait, and println!("{:?}", value) asks for Debug — neither exists until you provide it, usually with #[derive(Debug)]:
// @expect-error E0277struct Point { x: f64, y: f64 }
fn main() { let p = Point { x: 1.0, y: 2.0 }; println!("{}", p);}error[E0277]: `Point` doesn't implement `std::fmt::Display` --> src/main.rs:5:20 |5 | println!("{}", p); | -- ^ `Point` cannot be formatted with the default formatter | | | required by this formatting parameter |help: the trait `std::fmt::Display` is not implemented for `Point` --> src/main.rs:1:1 |1 | struct Point { x: f64, y: f64 } | ^^^^^^^^^^^^ = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
error: aborting due to 1 previous errorThe compiler’s own suggestion — try {:?} — only moves the problem one step, since {:?} needs Debug, which is equally absent until you #[derive(Debug)]. This is not a special-case restriction on structs and enums: it is the same “no implicit conversion, no implicit anything” rule you will see throughout Rust. A type is exactly what you declared it to be, nothing more, until you opt in.
Mental Model
Section titled “Mental Model”A struct is a fixed-shape record: every instance has every field, always. An enum is a choice between shapes: every instance is exactly one of its variants, and different variants can carry completely different data. match is how you get the compiler to prove you handled every one of those shapes — it is not a runtime if/else if chain in disguise, it is a static check that runs at compile time, before your program ever executes.
Trade-offs
Section titled “Trade-offs”A struct costs you nothing to model “always-present, fixed fields,” but it cannot represent “one of several alternatives” without either an awkward set of Option<T> fields (most of which are None at any given time) or an embedded enum field — which is exactly what an enum is for. An enum costs you a match (or if let) at every point you need to use the value, since the compiler will not let you assume which variant you have; in exchange, adding a new variant later surfaces every place that needs updating, as a compile error, instead of a runtime surprise.
Common Misconceptions
Section titled “Common Misconceptions”- “An enum in Rust is basically the same as an enum in C or Java.” A C-style enum is a named integer. A Rust enum variant can carry its own data (
Circle(f64),Message::Move { x: i32, y: i32 }), making it closer to what other languages call a tagged union or algebraic data type. - “
matchis just a fancierswitchstatement.”switchfalls through by default and does not require every case to be handled.matchis an expression (it produces a value) and the compiler rejects it outright if any possible value of the matched type is left unhandled. - “I can print any struct with
{}once it derivesDebug.”{}asks forDisplay;{:?}asks forDebug. DerivingDebugnever satisfies a{}format specifier — they are two different traits with two different format specifiers.
Reading AI-generated code
Section titled “Reading AI-generated code”AI assistants often model a fixed set of choices as a &str compared with ==, because it “just works” without defining a new type — but the compiler cannot catch a typo in a string the way it catches a misspelled enum variant.
🐛 Spot the bug
fn move_player(direction: &str) { if direction == "north" { println!("moving north"); } else if direction == "south" { println!("moving south"); } else { println!("unknown direction: {}", direction); }}
fn main() { move_player("north"); move_player("nroth"); // typo: silently falls into the "unknown" branch}Show the answer
"nroth" compiles fine — it is a perfectly valid string — and silently falls through to the else branch at runtime. Nothing catches this until a human notices the wrong behavior. The fix is an enum Direction { North, South, East, West } matched exhaustively: Direction::Nrogh is not a typo the compiler lets through, because it is not one of the enum’s variants at all — rustc rejects it before the program ever runs.