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:
Option<T>isSome(T)orNone: a value that might be missing. Rust has no null.Result<T, E>isOk(T)orErr(E): an operation that can fail.
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
- Forgetting the enum name:
Quitalone is "cannot find valueQuit". WriteCommand::Quit, oruse Command::*;inside a function for short names. - Comparing with
==:if light == Light::Redis E0369, "binary operation==cannot be applied to typeLight", until you#[derive(PartialEq)]. Or usematches!(light, Light::Red). - Using
_ =>too eagerly. A catch-all arm switches off the exhaustiveness check: new variants silently fall into it. List variants explicitly when it matters. - C-style enums: variants without data can be cast to integers (
Level::Warn as u8), and you can set values (Warn = 30). Converting back from an integer needs amatchorTryFrom.
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