RustArena

Enums in Rust: variants that carry data

Rust enums explained: defining variants with and without data, matching on them, methods on enums, and modeling states that can't go wrong.

An enum (enumeration) is a type whose value is exactly one of a fixed set of variants. Unlike enums in C or Java, each Rust variant can carry its own data:

enum Command {
    Quit,                        // no data
    Move { x: i32, y: i32 },     // named fields, like a struct
    Say(String),                 // one unnamed value
    SetColor(u8, u8, u8),        // several unnamed values
}

A Command is either a Quit, or a Move with coordinates, or a Say with text, never a mix. You create values with the enum name and ::: Command::Say(String::from("hi")), Command::Move { x: 1, y: 2 }.

Using an enum: match

You get the data back out with match, which runs the arm whose pattern fits and binds the variant's data to names:

enum Command {
    Quit,
    Move { x: i32, y: i32 },
    Say(String),
}

fn run(cmd: &Command) -> String {
    match cmd {
        Command::Quit => String::from("bye"),
        Command::Move { x, y } => format!("moving to ({x}, {y})"),
        Command::Say(text) => format!("saying {text:?}"),
    }
}

fn main() {
    let script = [Command::Say(String::from("hi")), Command::Move { x: 3, y: -1 }, Command::Quit];
    for cmd in &script {
        println!("{}", run(cmd));
    }
}
saying "hi"
moving to (3, -1)
bye

match is exhaustive: forget a variant and the code doesn't compile (E0004, "non-exhaustive patterns: Command::Quit not covered"). When you add a variant later, the compiler lists every match you need to update. That's the biggest practical advantage of enums.

Methods on enums

Enums get impl blocks just like structs: impl Command { fn is_quit(&self) -> bool { matches!(self, Command::Quit) } }. matches! returns true if a value fits a pattern.

The enums you use every day

The standard library's two most important types are enums:

Enums vs structs, and designing with them

Use a struct when a value has all of several fields; use an enum when it's one of several shapes. A classic design mistake is a struct with flags and optional fields that only make sense in some combinations:

struct Order { shipped: bool, tracking: Option<String>, cancelled: bool, reason: Option<String> }

Nothing stops shipped: true, cancelled: true. An enum makes invalid states impossible to write:

enum OrderState { Pending, Shipped { tracking: String }, Cancelled { reason: String } }

Common mistakes

Example

#[derive(Debug, Clone, Copy, PartialEq)]
enum Level {
    Info,
    Warn,
    Error,
}

#[derive(Debug)]
enum OrderState {
    Pending,
    Shipped { tracking: String },
    Cancelled { reason: String },
}

impl OrderState {
    fn describe(&self) -> String {
        match self {
            OrderState::Pending => String::from("waiting to ship"),
            OrderState::Shipped { tracking } => format!("shipped, tracking {tracking}"),
            OrderState::Cancelled { reason } => format!("cancelled: {reason}"),
        }
    }

    fn is_open(&self) -> bool {
        matches!(self, OrderState::Pending | OrderState::Shipped { .. })
    }
}

fn main() {
    let orders = vec![
        OrderState::Pending,
        OrderState::Shipped { tracking: String::from("RS-1042") },
        OrderState::Cancelled { reason: String::from("out of stock") },
    ];
    for (i, o) in orders.iter().enumerate() {
        println!("order {}: {} (open: {})", i + 1, o.describe(), o.is_open());
    }
    println!("{:?}", orders[1]);

    let level = Level::Warn;
    if level == Level::Warn {
        println!("{level:?} has code {}", level as u8);
    }
    println!("{:?} {:?}", Level::Info, Level::Error);
}

Output:

order 1: waiting to ship (open: true)
order 2: shipped, tracking RS-1042 (open: true)
order 3: cancelled: out of stock (open: false)
Shipped { tracking: "RS-1042" }
Warn has code 1
Info Error

Practice it