Enums and match

Rust · Essentials · 12 min

Enums with data plus exhaustive match are Rust's main tool for making invalid states unrepresentable.

An enum defines a type whose value is exactly one of several variants. Unlike enums in C or Java, each variant can carry its own data: none, a tuple of values, or named fields.

enum Shape {
    Circle { radius: f64 },
    Rect(f64, f64),
    Empty,
}

You create a value with Shape::Rect(2.0, 3.0), and you can add methods in an impl block just like with structs.

`match` compares a value against patterns from top to bottom and runs the first arm that matches. Patterns can destructure a variant and bind its data to names. match is exhaustive: if you forget a variant, the program does not compile. That is a big deal in practice: when you add a variant later, the compiler lists every match that must handle it.

The _ pattern matches anything and is the catch-all. Use it deliberately, because it also silences the helpful exhaustiveness errors when new variants are added.

match is an expression, so all arms must produce the same type, and you can assign its result directly. Arms are separated by commas; a block arm { ... } can hold several statements.

When matching on a reference (match &shape), patterns bind references to the inner data automatically, so you can inspect a value without moving out of it.

This style of data, called a sum type or tagged union, is used everywhere in Rust: messages, states of a state machine, AST nodes, and the standard Option and Result types.

Example

enum Shape {
    Circle { radius: f64 },
    Rect(f64, f64),
    Empty,
}
impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rect(w, h) => w * h,
            Shape::Empty => 0.0,
        }
    }
}

fn main() {
    let shapes = [Shape::Rect(2.0, 3.0), Shape::Circle { radius: 1.0 }, Shape::Empty];
    for s in &shapes {
        let label = match s {
            Shape::Empty => "nothing",
            _ => "something",
        };
        println!("{label}: {:.2}", s.area()); // something: 6.00 / 3.14 / nothing: 0.00
    }
}

Remember

  • Each enum variant can carry different data
  • match is exhaustive: every variant must be handled
  • Avoid _ catch-alls where you want the compiler to flag new variants

Watch out for

  • Using a struct with many optional fields and a kind string where an enum would make illegal states impossible.

Check yourself

  1. You add a variant `Triangle` to `Shape`. What happens to an existing `match` that lists every variant without `_`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch06-02-match.html