Enums and Pattern Matching

Enums with Associated Data

Unlike enums in C, C++, or Java—where variants are limited to integer tags or shared class fields—each variant of a Rust enum can carry its own distinct associated data (or no data at all). Attaching data directly to the variant that uses it makes illegal states unrepresentable at compile time.

Making Illegal States Unrepresentable

Consider modeling a network connection that is either Disconnected, Connecting, or Connected. In a struct-based design with nullable/optional fields (struct Connection { state: State, socket: Option<Socket>, retry_count: Option<u32> }), nothing prevents a bug where state == Disconnected yet socket is still Some(...).

With a Rust enum, each state carries only the data that is valid in that exact state:

enum ConnectionState {
    Disconnected,                                   (Carries 0 extra fields)
    Connecting { attempt: u32 },                    (Only exists while connecting!)
    Connected { address: String, latency_ms: u32 }, (Only exists while connected!)
}
#[derive(Debug)]
enum ConnectionState {
    Disconnected,
    Connecting { attempt: u32 },
    Connected { address: String, latency_ms: u32 },
}

impl ConnectionState {
    fn status_line(&self) -> String {
        match self {
            Self::Disconnected => String::from("Offline"),
            Self::Connecting { attempt } => format!("Connecting (attempt #{attempt})..."),
            Self::Connected { address, latency_ms } => {
                format!("Connected to {address} ({latency_ms} ms)")
            }
        }
    }
}

fn main() {
    let states = [
        ConnectionState::Disconnected,
        ConnectionState::Connecting { attempt: 2 },
        ConnectionState::Connected {
            address: String::from("10.0.0.42:443"),
            latency_ms: 14,
        },
    ];

    for s in &states {
        println!("{}", s.status_line());
    }
}
Offline
Connecting (attempt #2)...
Connected to 10.0.0.42:443 (14 ms)

Modifying Associated Data in Place (&mut self)

By matching on a mutable reference (match self where self: &mut Enum), pattern bindings automatically become mutable references (&mut T) to the variant's associated data, allowing you to update a variant's inner fields in place:

#[derive(Debug)]
enum CounterState {
    Paused(u32),
    Running(u32),
}

impl CounterState {
    fn tick(&mut self) {
        if let Self::Running(count) = self {
            // `count` has type `&mut u32`, modifying the enum's payload in place!
            *count += 1;
        }
    }
}

fn main() {
    let mut timer = CounterState::Running(10);
    timer.tick();
    timer.tick();
    println!("After 2 ticks while Running: {timer:?}");

    let mut paused = CounterState::Paused(10);
    paused.tick();
    println!("After tick while Paused:     {paused:?}");
}
After 2 ticks while Running: Running(12)
After tick while Paused:     Paused(10)

Recursive Enums with Associated Data (Box<T>)

Enums with associated data are the standard way to represent recursive trees—such as JSON documents (JsonValue), Abstract Syntax Trees (Expr), and linked structures—by wrapping recursive child expressions in Box<T> or Vec<T>:

#[derive(Debug)]
enum Expr {
    Literal(i64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

impl Expr {
    fn eval(&self) -> i64 {
        match self {
            Self::Literal(n) => *n,
            Self::Add(left, right) => left.eval() + right.eval(),
            Self::Mul(left, right) => left.eval() * right.eval(),
        }
    }
}

fn main() {
    // Represents `(2 + 3) * 4`:
    let ast = Expr::Mul(
        Box::new(Expr::Add(Box::new(Expr::Literal(2)), Box::new(Expr::Literal(3)))),
        Box::new(Expr::Literal(4)),
    );

    println!("Evaluated AST: {}", ast.eval());
}
Evaluated AST: 20