Enums and Pattern Matching
A struct says "a value has all of these fields". Just as often you need to say
"a value is exactly one of these things": a payment is by cash, card or bank
transfer; a network request is pending, finished or failed. You could use a
number or a string for that, but then nothing stops a typo like "faild", and
nothing reminds you to handle every case.
An enum (short for enumeration) defines a type by listing its possible
variants. Rust's enums can also carry data in each variant, and
pattern matching with match takes them apart safely. Together they are
one of the features Rust code leans on most.
Defining an enum
The simplest enum lists variants with no data:
#[derive(Debug)]
enum Direction {
North,
East,
South,
West,
}
fn main() {
let heading = Direction::East;
println!("heading {heading:?}");
let route = [Direction::North, Direction::West, Direction::South];
println!("{route:?}");
}heading East
[North, West, South]- Enum and variant names are
UpperCamelCase. - A variant is written with the enum's name, a
::and the variant:Direction::East. headinghas the typeDirection. ADirectionis always exactly one of the four variants and can never be anything else.#[derive(Debug)]works on enums just as on structs, and prints the variant name.
match
To act on an enum, use match. It compares a value against a list of
patterns and runs the arm of the first one that fits:
enum Direction {
North,
East,
South,
West,
}
fn arrow(direction: Direction) -> char {
match direction {
Direction::North => '↑',
Direction::East => '→',
Direction::South => '↓',
Direction::West => '←',
}
}
fn main() {
let route = [
Direction::North,
Direction::East,
Direction::South,
Direction::West,
];
for direction in route {
print!("{}", arrow(direction));
}
println!();
}↑→↓←Each arm is pattern => expression, separated by commas. Like if, match
is an expression: its value is the value of the arm that ran, so every arm has
to produce the same type. Here the match is the last expression of arrow,
so its value is what the function returns.
The key property of match is that it is exhaustive: the arms together
must cover every possible value. If you add a fifth variant to Direction,
every match on it that doesn't handle the new variant stops compiling,
which points you at each place that needs updating.
Several patterns, blocks and the catch-all
An arm can match several patterns joined with |, and its expression can be a
block when it needs more than one line. The pattern _ matches anything, so
it can cover every variant you didn't list:
enum Weekday {
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
Sun,
}
fn plan(day: Weekday) -> &'static str {
match day {
Weekday::Sat | Weekday::Sun => "rest",
Weekday::Fri => {
println!("(payday)");
"work, then pizza"
}
_ => "work",
}
}
fn main() {
let week = [
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
Weekday::Sat,
Weekday::Sun,
];
for day in week {
println!("{}", plan(day));
}
}work
work
work
work
(payday)
work, then pizza
rest
restArms are checked from top to bottom, so _ goes last. Use it with care: a _
arm also swallows variants added later, so you lose the reminder that
exhaustiveness gives you.
(&'static str is the type of a string literal. The 'static part is a
lifetime, which has its own lesson.)
Variants that carry data
Each variant can hold its own data, in any of the three shapes you know from structs: none (like a unit struct), unnamed fields (like a tuple struct) or named fields (like a regular struct).
A pattern can mirror the shape of a variant and bind names to its data, and those names can then be used in the arm:
#[derive(Debug)]
enum Payment {
Cash,
Card { last4: u16, contactless: bool },
Transfer(String),
}
fn receipt_line(payment: &Payment) -> String {
match payment {
Payment::Cash => String::from("paid in cash"),
Payment::Card { last4, contactless } => {
let how = if *contactless { "tap" } else { "chip" };
format!("card ending {last4} ({how})")
}
Payment::Transfer(iban) => format!("transfer from {iban}"),
}
}
fn main() {
let payments = [
Payment::Cash,
Payment::Card {
last4: 1881,
contactless: false,
},
Payment::Transfer(String::from("NL91 ABNA")),
];
for payment in &payments {
println!("{payment:?}");
println!(" {}", receipt_line(payment));
}
}Cash
paid in cash
Card { last4: 1881, contactless: false }
card ending 1881 (chip)
Transfer("NL91 ABNA")
transfer from NL91 ABNAAll three are values of the same type, Payment, which is why they fit in one
array. Written with structs instead, you would need three separate types and
no single type that can be any of them.
Payment::Transfer(iban)binds theStringinside toiban.Payment::Card { last4, contactless }binds both fields by name. You can rename one withlast4: digits, or skip the rest with.., as inPayment::Card { last4, .. }.receipt_linetakes&Payment, so matching a reference gives references to the data:ibanis a&Stringandcontactlessis a&bool. That is why theifreads*contactless. The payment itself is only borrowed, and the caller keeps it.
Methods on enums
An enum can have an impl block with methods and associated functions, just
like a struct. Inside a method, match self looks at which variant the value
is:
enum Job {
Queued,
Running { progress: u8 },
Failed(String),
Done,
}
impl Job {
fn is_finished(&self) -> bool {
match self {
Job::Done | Job::Failed(_) => true,
Job::Queued | Job::Running { .. } => false,
}
}
fn status(&self) -> String {
match self {
Job::Queued => String::from("waiting"),
Job::Running { progress } => format!("{progress}%"),
Job::Failed(reason) => format!("error: {reason}"),
Job::Done => String::from("done"),
}
}
fn advance(&mut self) {
*self = match self {
Job::Queued => Job::Running { progress: 0 },
Job::Running { progress } if *progress >= 50 => Job::Done,
Job::Running { progress } => Job::Running {
progress: *progress + 50,
},
Job::Failed(reason) => Job::Failed(reason.clone()),
Job::Done => Job::Done,
};
}
}
fn main() {
let mut job = Job::Queued;
while !job.is_finished() {
println!("{}", job.status());
job.advance();
}
println!("{}", job.status());
let broken = Job::Failed(String::from("disk full"));
println!("{} {}", broken.status(), broken.is_finished());
}waiting
0%
50%
done
error: disk full true_ inside a pattern ignores one value, and { .. } ignores every named field.
advance takes &mut self and replaces the whole value with *self = ...,
which is how an enum moves from one variant to another. The if in one of its
arms is a guard, covered below.
if let: matching a single pattern
When you only care about one variant, a full match with a _ => () arm is
noisy. if let runs a block only if a value matches one pattern, binding its
data on the way:
enum Job {
Queued,
Running { progress: u8 },
Failed(String),
}
fn main() {
let jobs = [
Job::Running { progress: 30 },
Job::Failed(String::from("timeout")),
Job::Queued,
];
for job in &jobs {
if let Job::Failed(reason) = job {
println!("retrying after: {reason}");
} else if let Job::Running { progress } = job {
println!("still running at {progress}%");
} else {
println!("nothing to do");
}
}
}still running at 30%
retrying after: timeout
nothing to doif let PATTERN = VALUE reads as "if VALUE matches PATTERN". It can be
followed by else if let, else if or else, like any if. Unlike match,
it isn't exhaustive, so the compiler won't point out a variant you forgot.
That is the trade-off: less typing for one case, no safety net for the rest.
let else
The opposite shape is also common: you need one variant to carry on, and
everything else means leaving early. let ... else binds the data if the
pattern matches and otherwise runs a block that must leave the function (with
return, break, continue or a panic):
enum Job {
Running { progress: u8 },
Done,
}
fn remaining(job: &Job) -> u8 {
let Job::Running { progress } = job else {
return 0;
};
100 - progress
}
fn main() {
println!("{}", remaining(&Job::Running { progress: 70 }));
println!("{}", remaining(&Job::Done));
}30
0After the let else, progress is in scope for the rest of the function,
without an extra level of indentation.
matches!
To turn "does this match a pattern?" into a bool, use the matches! macro:
enum Job {
Queued,
Paused,
Done,
}
fn main() {
let jobs = [Job::Queued, Job::Paused, Job::Done];
let active = jobs
.iter()
.filter(|job| matches!(job, Job::Queued | Job::Paused))
.count();
println!("{active} active");
let done = matches!(jobs[2], Job::Done);
println!("{done}");
}2 active
true(The .iter().filter(...) chain counts matching items; iterators and the
|job| ... closure syntax come later in the track.)
More patterns
Patterns aren't only for enums. match works on numbers, characters,
strings, tuples and structs, and patterns can be combined and nested.
Literals, ranges and tuples
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1 | 2 | 3 => "a few",
4..=9 => "several",
i32::MIN..=-1 => "negative",
_ => "lots",
}
}
fn quadrant(point: (i32, i32)) -> &'static str {
match point {
(0, 0) => "origin",
(_, 0) | (0, _) => "on an axis",
(x, y) if (x > 0) == (y > 0) => "first or third",
_ => "second or fourth",
}
}
fn main() {
for n in [0, 2, 7, -5, 100] {
println!("{n}: {}", describe(n));
}
println!("{}", quadrant((3, 0)));
println!("{}", quadrant((-2, -8)));
println!("{}", quadrant((-2, 8)));
}0: zero
2: a few
7: several
-5: negative
100: lots
on an axis
first or third
second or fourth4..=9matches any value in the inclusive range. Ranges work for integers andchars.- Matching on a tuple lets one
matchdecide on two values at once._ignores a position you don't care about. - For integers, exhaustiveness still applies: without the final
_, the compiler would point out that10..=i32::MAXisn't covered.
Match guards
A guard is an extra if condition after a pattern. The arm only runs when
the pattern matches and the guard is true; otherwise matching carries on
with the next arm:
enum Reading {
Temperature(f64),
Humidity(u8),
Offline,
}
fn alert(reading: &Reading) -> Option<String> {
match reading {
Reading::Temperature(t) if *t > 40.0 => Some(format!("too hot: {t}")),
Reading::Temperature(t) if *t < 0.0 => Some(format!("freezing: {t}")),
Reading::Humidity(h) if *h > 90 => Some(format!("damp: {h}%")),
Reading::Offline => Some(String::from("sensor offline")),
_ => None,
}
}
fn main() {
let readings = [
Reading::Temperature(22.5),
Reading::Temperature(-3.0),
Reading::Humidity(95),
Reading::Humidity(40),
Reading::Offline,
];
for r in &readings {
if let Some(message) = alert(r) {
println!("{message}");
}
}
}freezing: -3
damp: 95%
sensor offlineGuards can use any expression, including the names the pattern bound (which
are references here, hence *t and *h). The compiler doesn't look inside
guards when it checks exhaustiveness, so a match with guarded arms usually
needs a final arm without one.
Option is an enum from the standard library with two variants, Some(value)
and None. It is how Rust says "there might not be a value", and it has a
lesson of its own; here it lets alert say "no alert".
Binding with @
A pattern like 13..=19 tests a value but doesn't give it a name. Writing
name @ pattern does both:
fn label(age: u32) -> String {
match age {
0 => String::from("newborn"),
teen @ 13..=19 => format!("teenager ({teen})"),
n @ (1..=12 | 20..) => format!("{n} years old"),
}
}
fn main() {
for age in [0, 7, 16, 42] {
println!("{}", label(age));
}
}newborn
7 years old
teenager (16)
42 years oldThis match needs no _: 0, 1..=12, 13..=19 and 20.. cover every
u32 between them, and the compiler checks that.
Nested patterns
Patterns nest as deeply as the data does, so one arm can look through several layers at once:
enum Shape {
Circle { radius: f64 },
Square(f64),
}
enum Item {
Label(String),
Sticker(Shape, u32),
}
fn price(item: &Item) -> u32 {
match item {
Item::Label(text) if text.is_empty() => 0,
Item::Label(text) => 50 + text.len() as u32 * 10,
Item::Sticker(Shape::Circle { radius }, count) if *radius > 5.0 => 300 * count,
Item::Sticker(Shape::Circle { .. }, count) => 150 * count,
Item::Sticker(Shape::Square(side), count) => *side as u32 * 40 * count,
}
}
fn main() {
let order = [
Item::Label(String::from("fragile")),
Item::Sticker(Shape::Circle { radius: 8.0 }, 2),
Item::Sticker(Shape::Square(3.0), 5),
];
for item in &order {
println!("{}", price(item));
}
}120
600
600Comparing enums with PartialEq
if payment == Payment::Cash looks natural, but it doesn't compile for an
enum you wrote: Rust doesn't assume what "equal" means for your types. Derive
the PartialEq trait to get the obvious meaning, same variant and equal data:
#[derive(Debug, PartialEq)]
enum Seat {
Aisle(u8),
Window(u8),
Standby,
}
fn main() {
let mine = Seat::Window(12);
let yours = Seat::Window(12);
let theirs = Seat::Aisle(12);
println!("{}", mine == yours);
println!("{}", mine == theirs);
println!("{}", Seat::Standby != mine);
assert_eq!(mine, Seat::Window(12));
}true
false
true- Two values are equal when they are the same variant and all their data is
equal, so
Window(12) != Aisle(12). - Deriving
PartialEqonly works if every piece of data inside supports==too. Numbers,bool,charandStringall do. assert_eq!needs bothPartialEq(to compare) andDebug(to print the values if they differ), which is why derives usually come in groups:#[derive(Debug, PartialEq)].
PartialEq works the same way on structs. When you only need to check the
variant, matches! is an alternative that doesn't need the derive and ignores
the data: matches!(mine, Seat::Window(_)).
Common mistakes
A match that misses a variant
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}
fn color(suit: Suit) -> &'static str {
match suit {
Suit::Diamonds | Suit::Hearts => "red",
Suit::Clubs => "black",
}
}
fn main() {
println!("{}", color(Suit::Spades));
}The compiler lists the variants you missed and suggests an arm to add. Add
Suit::Spades to the "black" arm; resist reaching for _, which would also
hide any suit added later.
Comparing without PartialEq
enum Mode {
Light,
Dark,
}
fn main() {
let mode = Mode::Dark;
if mode == Mode::Light {
println!("light");
}
}Add #[derive(PartialEq)] above enum Mode, or check the variant with
matches!(mode, Mode::Light).
Forgetting the enum name
enum Mode {
Light,
Dark,
}
fn main() {
let mode = Dark;
match mode {
Mode::Light => println!("light"),
Mode::Dark => println!("dark"),
}
}Variants live inside their enum, so they are Mode::Dark, not Dark. The
compiler suggests importing the variant with use, which works (and
use Mode::*; would bring them all into scope), but writing Mode::Dark is
clearer and is what most code does.
Moving data out with if let
enum Upload {
Pending,
Ready(String),
}
fn main() {
let upload = Upload::Ready(String::from("photo.png"));
if let Upload::Ready(path) = upload {
println!("sending {path}");
}
if let Upload::Pending = upload {
println!("still pending");
}
}Matching upload by value moves the String out of it into path, so
upload can't be used afterwards. Match on a reference instead,
if let Upload::Ready(path) = &upload, and path becomes a &String that
borrows from upload. The compiler's suggestion, ref path, does the same
thing from inside the pattern.
Summary
- An enum lists the variants a value can be:
enum Mode { Light, Dark }. Write a variant asMode::Dark. - Variants can carry data, shaped like unit, tuple or named-field structs, and different variants can carry different data.
matchruns the first arm whose pattern fits, binds the data it names, and must cover every possible value._matches anything and|joins patterns.- Matching on a reference binds references to the data, leaving the value where it is.
if lethandles one pattern,let elsebails out early when a pattern doesn't match, andmatches!gives you abool.- Patterns can be literals, ranges (
4..=9), tuples and nested variants, with_and..to ignore parts,ifguards for extra conditions andname @ patternto bind what matched. #[derive(PartialEq)]lets you compare enums (and structs) with==; addDebugtoo to useassert_eq!.
Practise what you read
Challenges that use the ideas from this lesson.