11.xChapter 11 summary and quiz
Chapter 10 gave you "and" types; this chapter gave you "or" types and the pattern-matching machinery that makes them sing. Review, then a capstone that wires several pieces together.
Quick review
An enum is a program-defined type whose value is exactly one of a fixed set of variants (11.1). Reach for one whenever a value has a known set of states, especially where a bool is tempting but doesn't fit. The payoff is exhaustiveness: a match over an enum must handle every variant (E0004 otherwise), so adding a variant later turns every unhandled match into a compiler-assigned checklist. This is "make impossible states unrepresentable."
Variants can carry data, and different variants can carry different shapes: nothing, a tuple of values, or named fields (11.2). An enum with data is "a value that is one of several shapes," the sum-type partner to chapter 10's product-type structs. You read the data back out by matching, never with a dot, because the data is only present in some variants.
Option<T> is the standard-library enum with variants Some(T) and None, Rust's replacement for null (11.3). Because Option<T> is a different type from T, the compiler forces you to handle absence before use, moving the null check from runtime hope to compile-time guarantee. unwrap/expect convert "maybe absent" into "present or panic" and are the long-promised explanation of the .expect() you've written since lesson 1.6; prefer match, the if let family, or unwrap_or for Options that can really be None.
match in depth (11.4): patterns bind names (Some(n)), combine with |, test ranges with ..=, carry extra conditions as guards (if x > 0), and bind-while-testing with @. The two catch-alls are _ (ignore) and a bare name (bind), and they must come last. Destructuring (11.5) is the same patterns aimed at taking compound values apart, in match, in let, and in function parameters; construction and destructuring are mirror images. A let pattern must match every value, which is why let Some(x) = ...; is refused.
That refusal motivates three lighter tools (11.6): if let for one pattern (plus optional else), while let for looping until a pattern fails, and let else for bind-or-bail early exit. Each is a match with the boilerplate removed; use them for readability, use match when you want every case checked. Finally, enums take methods via impl just like structs, and the match self shape inside them models state machines, where variants are states, carried data is per-state data, and methods are transitions (11.7).
Quiz time
Question #1
When should you reach for an enum instead of a struct? Give the one-sentence test.
Show solution
Use an enum when a value is exactly one of a fixed set of alternatives (the "or" question); use a struct when a value's parts all coexist (the "and" question). A traffic light is one of red/yellow/green: enum. A point has both an x and a y: struct.
Question #2
For each, say what it prints or why it's refused:
a)
let x: Option<i32> = Some(4);
let y = x + 1;
b)
let x: Option<i32> = Some(4);
println!("{}", x.unwrap_or(0));
c)
let x: Option<i32> = None;
if let Some(n) = x {
println!("{n}");
} else {
println!("none");
}Show solution
a) Refused, E0277: Option<i32> is not i32, so you can't add 1 to it; you must handle the None case first. b) Prints 4: unwrap_or returns the contained value since it's Some. c) Prints none: x is None, the if let pattern doesn't match, so the else runs.
Question #3
What does this print?
fn classify(n: i32) -> &'static str {
match n {
0 => "zero",
x if x < 0 => "negative",
1..=9 => "small",
big @ 10..=99 => return_label(big),
_ => "huge",
}
}
fn return_label(_n: i32) -> &'static str {
"medium"
}
fn main() {
println!("{}", classify(-5));
println!("{}", classify(7));
println!("{}", classify(50));
println!("{}", classify(1000));
}Show solution
negative
small
medium
huge
-5 fails 0, matches the guard x < 0. 7 matches 1..=9. 50 matches big @ 10..=99, binding 50 and passing it to return_label, which returns "medium". 1000 falls through to _. (The &'static str return type is the whole-program lifetime of string literals from lesson 9.5.)
Question #4
The capstone. Model a vending machine as a state machine. Define an enum Machine with variants Waiting, Collecting { inserted: u32 } (cents inserted so far), and Dispensing { item: String }. Write:
- a method
insert(self, cents: u32) -> Machinethat, fromWaitingorCollecting, adds to the inserted total and returnsCollecting(from any other state, returnsselfunchanged); - a method
status(&self) -> Stringdescribing the current state; - in
main, start atWaiting, insert 75 then 50, and print the status after each step.
Show solution
#[derive(Debug)]
enum Machine {
Waiting,
Collecting { inserted: u32 },
Dispensing { item: String },
}
impl Machine {
fn insert(self, cents: u32) -> Machine {
match self {
Machine::Waiting => Machine::Collecting { inserted: cents },
Machine::Collecting { inserted } => {
Machine::Collecting { inserted: inserted + cents }
}
other => other,
}
}
fn status(&self) -> String {
match self {
Machine::Waiting => String::from("insert coins"),
Machine::Collecting { inserted } => format!("{inserted} cents in"),
Machine::Dispensing { item } => format!("dispensing {item}"),
}
}
}
fn main() {
let mut machine = Machine::Waiting;
println!("{}", machine.status());
machine = machine.insert(75);
println!("{}", machine.status());
machine = machine.insert(50);
println!("{}", machine.status());
}insert coins
75 cents in
125 cents in
Design notes. insert takes self by value (not &self) because it consumes the old state and returns a new one, which is the state-machine transition shape; that's why main reassigns machine each step and needs a mut binding. Inserting 75 from Waiting gives Collecting { inserted: 75 }, and inserting another 50 gives Collecting { inserted: 125 }. The other => other arm forwards any unhandled state unchanged, and status takes &self because it only reads. The Dispensing variant is unused in this run but completes the machine; a fuller version would transition into it once inserted reaches an item's price.
You now hold both halves of Rust's type-building toolkit: structs for "and", enums for "or", and match to take either apart safely. Chapter 12 spends that toolkit on the problem it was secretly built for. Result, the enum hiding inside every parse and read_line you've written, becomes error handling you design deliberately, with the ? operator turning pages of boilerplate into a single character.