5 min read #rust #Type System
On this page

Enums and Pattern Matching

Rust’s enums are tagged unions—each variant carries its own data, and the compiler ensures that match expressions cover all variants (exhaustiveness checking). Option and Result are not built into the language; they are defined in the standard library using enums—demonstrating that enums are Rust’s most expressive type constructor.

Enums are Tagged Unions, Not Just "Enumerations"

C’s enums are merely a set of named integer constants. In Rust, each variant of an enum can carry different data—it is a tagged union:

enum Message {
    Quit,                            // No data — pure tag
    Move { x: i32, y: i32 },         // Anonymous struct
    Write(String),                   // Tuple variant
    ChangeColor(u8, u8, u8),         // Tuple of 3 u8s
}

The compiler allocates enough space for Message to hold the largest variant (Move = two i32s = 8 bytes), plus a discriminant (tag) field. For cases with a niche, such as Option<&T>, the tag can be optimized away.

Option: Rust Has No Null

Tony Hoare called null the "billion-dollar mistake." Rust has no null—values that might not exist are represented by Option<T>:

fn safe_div(a: i32, b: i32) -> Option<i32> {
    if b == 0 { None } else { Some(a / b) }
}
// Callers must handle None:
match safe_div(10, 0) {
    Some(result) => println!("{}", result),
    None => println!("division by zero"),
}

The compiler forces you to handle the None case—you cannot forget to check. Option<T> is an ordinary type, not a special null marker, so you can use combinators like map and and_then on Option.

Result: Errors Are Values

The problem with exceptions is that they break control flow—you cannot tell from a function signature what exceptions it might throw. Result<T, E> makes errors part of the return value:

fn parse(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse()                        // Returns Result, does not throw
}

When callers see Result<i32, ParseIntError>, they know that this function may successfully return an i32 or fail with a parse error. There is no hidden control flow.

Match: Exhaustiveness Checking at Compile Time

match number {
    1 => "one",
    2 => "two",
    // _ => "other",                 // If this line is missing, compilation fails:
                                     // "non-exhaustive patterns"
}

The compiler checks whether the match covers all possible variants. If a new variant is added to the type (e.g., a new member to an enum), every match expression involving that type will fail to compile—forcing you to explicitly handle the new case.

Various Forms of Patterns

// Destructuring
let (x, y, z) = (1, 2, 3);
let Point { x, y } = point;

// ref pattern: does not move, borrows instead
let ref r = value;                   // r: &T, value is not moved

// Match guard: additional condition
match num {
    n if n % 2 == 0 => "even",
    n if n > 10 => "big",
    _ => "other",
}

// @ binding: matches the pattern and binds the variable simultaneously
match opt {
    Some(v @ 1..=5) => println!("small: {}", v),
    Some(v) => println!("large: {}", v),
    None => (),
}

// if let / while let: syntactic sugar for when you only care about one variant
if let Some(v) = opt { process(v); }
while let Some(v) = iter.next() { process(v); }

References

  • Rust Book: Chapter 6 (Enums), Chapter 18 (Patterns)
  • Rust Reference: Match expressions, Patterns

Keywords: enum, Option, Result, match, pattern, if let, while let, exhaustiveness, tagged union