Lesson 10 of 22·18 min

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.
  • heading has the type Direction. A Direction is 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
rest

Arms 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 ABNA

All 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 the String inside to iban.
  • Payment::Card { last4, contactless } binds both fields by name. You can rename one with last4: digits, or skip the rest with .., as in Payment::Card { last4, .. }.
  • receipt_line takes &Payment, so matching a reference gives references to the data: iban is a &String and contactless is a &bool. That is why the if reads *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 do

if 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
0

After 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 fourth
  • 4..=9 matches any value in the inclusive range. Ranges work for integers and chars.
  • Matching on a tuple lets one match decide 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 that 10..=i32::MAX isn'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 offline

Guards 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 old

This 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
600

Comparing 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 PartialEq only works if every piece of data inside supports == too. Numbers, bool, char and String all do.
  • assert_eq! needs both PartialEq (to compare) and Debug (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 as Mode::Dark.
  • Variants can carry data, shaped like unit, tuple or named-field structs, and different variants can carry different data.
  • match runs 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 let handles one pattern, let else bails out early when a pattern doesn't match, and matches! gives you a bool.
  • Patterns can be literals, ranges (4..=9), tuples and nested variants, with _ and .. to ignore parts, if guards for extra conditions and name @ pattern to bind what matched.
  • #[derive(PartialEq)] lets you compare enums (and structs) with ==; add Debug too to use assert_eq!.

Practise what you read

Challenges that use the ideas from this lesson.