Structs, Enums and Pattern Matching · about 45 min

Enums: Types with Variants

Why this matters

A struct says "a value has this AND that". Very often data is "this OR that": a payment is by card or by bank transfer or in cash; a shape is a circle or a rectangle; a network message is a login, a chat line or a logout, each carrying different data. Rust's enums model "one of several alternatives", each alternative optionally with its own data, and match handles them with compile-time checking that every case is covered. Enums are arguably Rust's most powerful everyday feature; Option and Result (next module) are enums.

Simple enums

#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction {
    North,
    East,
    South,
    West,
}

fn turn_right(d: Direction) -> Direction {
    match d {
        Direction::North => Direction::East,
        Direction::East => Direction::South,
        Direction::South => Direction::West,
        Direction::West => Direction::North,
    }
}

fn main() {
    let mut d = Direction::North;
    for _ in 0..5 {
        print!("{:?} ", d);
        d = turn_right(d);
    }
    println!();
    println!("{}", d == Direction::East);
}
North East South West North 
true

Variants with data

Each variant can carry its own data, in tuple form or struct form:

#[derive(Debug)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    Color(u8, u8, u8),
}

fn describe(m: &Message) -> String {
    match m {
        Message::Quit => String::from("quit"),
        Message::Move { x, y } => format!("move to ({x}, {y})"),
        Message::Write(text) => format!("write {text:?}"),
        Message::Color(r, g, b) => format!("color #{r:02x}{g:02x}{b:02x}"),
    }
}

fn main() {
    let msgs = [
        Message::Move { x: 3, y: -1 },
        Message::Write(String::from("hello")),
        Message::Color(255, 128, 0),
        Message::Quit,
    ];
    for m in &msgs {
        println!("{}", describe(m));
    }
}
move to (3, -1)
write "hello"
color #ff8000
quit

The patterns in match destructure each variant, binding its fields to names (x, y, text). Because we matched on a reference (m: &Message), the bindings are references too (text is a &String): matching borrows instead of moving. A Message value is always exactly one of the four variants, and only that variant's fields exist; there is no way to read x from a Write message.

Compare the alternative without enums: a struct with a kind field and every possible field, most of them meaningless for any given kind, and nothing stopping code from reading text on a Move. Enums make such invalid states unrepresentable.

Methods on enums

impl works on enums exactly as on structs:

enum Shape {
    Circle(f64),
    Square(f64),
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => 3.14159 * r * r,
            Shape::Square(s) => s * s,
        }
    }

    fn name(&self) -> &'static str {
        match self {
            Shape::Circle(_) => "circle",
            Shape::Square(_) => "square",
        }
    }
}

fn main() {
    let shapes = [Shape::Circle(1.0), Shape::Square(2.0)];
    let mut total = 0.0;
    for s in &shapes {
        println!("{} {:.2}", s.name(), s.area());
        total += s.area();
    }
    println!("total {:.2}", total);
}
circle 3.14
square 4.00
total 7.14

How an enum is stored

An enum value is a tagged union: a small integer tag (the discriminant) saying which variant it is, plus enough space for the largest variant's data. All variants share the same space.

enum Message { Quit, Move { x: i32, y: i32 }, Color(u8, u8, u8) }

  tag   payload (8 bytes, shared)
 +----+---------------------------+
 | 0  | (unused)                  |   Quit
 +----+---------------------------+
 | 1  | x (4 bytes) | y (4 bytes) |   Move
 +----+---------------------------+
 | 2  | r | g | b | (unused)      |   Color
 +----+---------------------------+
use std::mem::size_of;

#[allow(dead_code)]
enum Small { Quit, Move { x: i32, y: i32 }, Color(u8, u8, u8) }

#[allow(dead_code)]
enum WithString { Quit, Write(String) }

fn main() {
    println!("Small: {}", size_of::<Small>());
    println!("WithString: {}", size_of::<WithString>());
    println!("String: {}", size_of::<String>());
}
Small: 12
WithString: 24
String: 24

Small needs 8 bytes of payload plus a tag, padded to 12. WithString is the same size as a String: the compiler found bit patterns a String can never contain (its capacity can never exceed isize::MAX) and uses one of them to mean Quit. This niche optimization is why Option<&T> and Option<Box<T>> cost nothing extra, as you will see next module.

A match compiles to a check of the tag followed by a jump, often through a jump table: as fast as a C switch.

C-like enums and discriminants

Enums without data can be given explicit integer values and converted with as:

#[derive(Debug, Clone, Copy)]
enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    ServerError = 500,
}

fn main() {
    let s = HttpStatus::NotFound;
    println!("{:?} = {}", s, s as u16);
    println!("{}", HttpStatus::Ok as i32 + 1);
}
NotFound = 404
201

The reverse conversion (integer to enum) is not automatic, because most integers are not valid variants; you write a match for it, returning an Option for unknown values (next module).

Common mistakes

Non-exhaustive match: adding a variant breaks every match without a catch-all, which is the point:

enum Light { Red, Yellow, Green }

fn main() {
    let l = Light::Green;
    let go = match l {
        Light::Red => false,
        Light::Yellow => false,
    };
    println!("{go}");
}
error[E0004]: non-exhaustive patterns: `Light::Green` not covered
 --> main.rs:5:20
  |
5 |     let go = match l {
  |                    ^ pattern `Light::Green` not covered
  |
note: `Light` defined here
 --> main.rs:1:6
  |
1 | enum Light { Red, Yellow, Green }
  |      ^^^^^                ----- not covered
  = note: the matched value is of type `Light`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
7 ~         Light::Yellow => false,
8 ~         Light::Green => todo!(),
  |

Forgetting the type prefix: writing Red instead of Light::Red in a pattern does not refer to the variant; it creates a catch-all binding named Red (rustc warns about this: "pattern binding Red is named the same as one of the variants"). Always write Light::Red, or bring variants into scope with use Light::*;.

Comparing enums with == without PartialEq: E0369 "binary operation == cannot be applied". Derive PartialEq, or use match / matches!(value, Light::Red).

Moving data out of a borrowed enum: matching &Message binds references; trying to move the String out of a borrowed enum is E0507. Match on the owned value (match m { ... } where m: Message) to take it.

Recap

Going further: model a playing card with enum Suit and enum Rank inside a struct Card, and write fn beats(&self, other: &Card) -> bool. Then print size_of::<Option<String>>() and compare it with size_of::<String>().

Exercises

Exercise 1 · write code

Shapes and areas

Define

enum Shape {
    Circle { radius: f64 },
    Rect { w: f64, h: f64 },
    Triangle { base: f64, height: f64 },
}

with a method fn area(&self) -> f64 (use std::f64::consts::PI for circles). The given main reads lines like circle 1, rect 2 3, triangle 4 5 from stdin, builds a Shape for each and prints its area with 2 decimals:

3.14
6.00
10.00

File: main.rs

Starting point

use std::io;

fn main() {
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let w: Vec<&str> = line.split_whitespace().collect();
        // n(i) parses word i as a number (a small closure; closures are module 11)
        let n = |i: usize| -> f64 { w[i].parse().unwrap() };
        let shape = match w[0] {
            "circle" => Shape::Circle { radius: n(1) },
            "rect" => Shape::Rect { w: n(1), h: n(2) },
            _ => Shape::Triangle { base: n(1), height: n(2) },
        };
        println!("{:.2}", shape.area());
    }
}

Example

input

circle 1
rect 2 3
triangle 4 5

expected output

3.14
6.00
10.00
Hint 1

In area, match self { Shape::Circle { radius } => ..., ... } binds the fields by reference.

Hint 2

radius * radius works on &f64 values; the result is an f64.

Reference solution
use std::io;

enum Shape {
    Circle { radius: f64 },
    Rect { w: f64, h: f64 },
    Triangle { base: f64, height: f64 },
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rect { w, h } => w * h,
            Shape::Triangle { base, height } => 0.5 * base * height,
        }
    }
}

fn main() {
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let w: Vec<&str> = line.split_whitespace().collect();
        // n(i) parses word i as a number (a small closure; closures are module 11)
        let n = |i: usize| -> f64 { w[i].parse().unwrap() };
        let shape = match w[0] {
            "circle" => Shape::Circle { radius: n(1) },
            "rect" => Shape::Rect { w: n(1), h: n(2) },
            _ => Shape::Triangle { base: n(1), height: n(2) },
        };
        println!("{:.2}", shape.area());
    }
}

Exercise 2 · write code

A traffic light state machine

Define enum Light { Red, Green, Yellow } (derive Debug, Clone, Copy) with two methods:

Read a number n from stdin, start at Red, and print the first n states with their durations, then the total time:

Red 30
Green 25
Yellow 5
Red 30
total 90

for input 4. Print the state with {:?}.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    let n: u32 = s.trim().parse().unwrap();
}

Example

input

4

expected output

Red 30
Green 25
Yellow 5
Red 30
total 90

Example

input

0

expected output

total 0
Hint 1

#[derive(Debug, Clone, Copy)] above the enum makes it printable and copyable, so self methods do not consume your variable.

Hint 2

Each method is a match self { Light::Red => ..., Light::Green => ..., Light::Yellow => ... }.

Reference solution
use std::io;

#[derive(Debug, Clone, Copy)]
enum Light {
    Red,
    Green,
    Yellow,
}

impl Light {
    fn next(self) -> Light {
        match self {
            Light::Red => Light::Green,
            Light::Green => Light::Yellow,
            Light::Yellow => Light::Red,
        }
    }

    fn seconds(self) -> u32 {
        match self {
            Light::Red => 30,
            Light::Green => 25,
            Light::Yellow => 5,
        }
    }
}

fn main() {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    let n: u32 = s.trim().parse().unwrap();
    let mut light = Light::Red;
    let mut total = 0;
    for _ in 0..n {
        println!("{:?} {}", light, light.seconds());
        total += light.seconds();
        light = light.next();
    }
    println!("total {total}");
}

Exercise 3 · quiz

How big is an enum?

On a 64-bit machine, roughly how big is a value of this type?

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}
Answer
  • The size of the largest variant plus room for a tag saying which variant it is; here 24 bytes, because the tag fits into an unused bit pattern of the String

An enum value holds exactly one variant at a time, so it needs space for the largest one plus a discriminant (tag). Often the tag needs extra bytes; here rustc is clever and stores it in a value the String's capacity field can never have ("niche optimization"), so size_of::<Message>() is 24. The size is fixed at compile time, like every type that lives on the stack.

Exercise 4 · quiz

Adding a variant

A program has enum Status { Active, Suspended } and three match expressions on Status, none with a _ arm. You add a variant Deleted. What happens when you compile?

Answer
  • Each of the three matches fails with E0004, pointing out that Status::Deleted is not covered

Exhaustiveness checking turns "I added a case and forgot to handle it somewhere" into a list of compile errors at exactly the places that need attention. This is one of the most valuable features of Rust enums in large programs, and a reason to avoid _ arms on your own enums when you can list the variants explicitly.