Understanding Enums and Pattern Matching in Rust
Enums and pattern matching are central Rust features for representing a fixed set of possible states and handling each state explicitly. Rust enums can carry data, while match lets you destructure those values and choose behavior based on their shape.
What Is an Enum in Rust?
An enum defines a type whose value is exactly one of several variants. Unlike simple enumerations in some languages, Rust variants can also carry structured data.
Define and Use an Enum
enum TrafficLight {
Red,
Yellow,
Green,
}
fn main() {
let light = TrafficLight::Green;
match light {
TrafficLight::Red => println!("Stop!"),
TrafficLight::Yellow => println!("Caution!"),
TrafficLight::Green => println!("Go!"),
}
}TrafficLight has three variants, and match makes the behavior for each variant explicit.
Enums That Carry Data
Different variants can store different kinds of data:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn main() {
let msg = Message::Move { x: 10, y: 20 };
match msg {
Message::Quit => println!("The Quit variant contains no data."),
Message::Move { x, y } => println!("Move to coordinates: ({}, {})", x, y),
Message::Write(text) => println!("Message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to RGB: ({}, {}, {})", r, g, b),
}
}Here Move has named fields, Write carries a String, and ChangeColor stores a tuple of three u8 values. Matching can destructure each variant and bind its data to local names.
Pattern Matching with match
Patterns can match literals, alternatives, ranges, destructured values, and more:
fn describe_number(n: i32) -> &'static str {
match n {
1 => "One",
2 | 3 | 5 | 7 => "Prime",
13..=19 => "Teen",
_ => "Other",
}
}
fn main() {
println!("{}", describe_number(1));
println!("{}", describe_number(2));
println!("{}", describe_number(15));
println!("{}", describe_number(42));
}Important details:
|combines alternative patterns...=matches an inclusive range._matches any value not handled earlier.- A
matchmust cover every possible input.
Use if let for One Important Pattern
When you only care about one pattern, if let can be shorter than a full match:
fn main() {
let number = Some(7);
if let Some(n) = number {
println!("Number: {}", n);
} else {
println!("No number.");
}
}This is common with Option, Result, and application-specific enums.
Option and Result
Two of Rust’s most important standard enums are Option<T> and Result<T, E>.
Option<T> represents a value that may or may not exist:
fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn main() {
match divide(10, 2) {
Some(result) => println!("Result: {}", result),
None => println!("Cannot divide by zero."),
}
}Result<T, E> represents success or failure:
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("cannot divide by zero")
} else {
Ok(a / b)
}
}
fn main() {
match divide(10, 0) {
Ok(result) => println!("Result: {}", result),
Err(error) => println!("Error: {}", error),
}
}Best Practices
- Use enums when a value has a known set of distinct states or variants.
- Use pattern matching to keep handling of those states explicit.
- Prefer
OptionandResultover sentinel values for absence and recoverable errors. - Use
if letorlet elsewhen only one pattern needs special handling. - Use catch-all patterns deliberately so new or meaningful cases are not silently ignored.
Conclusion
Enums let Rust model state-rich data directly in the type system, and pattern matching provides a concise way to work with every possible form of that data. Together they are a foundation for expressive APIs, robust error handling, and clear application state machines.