#10 Enums and pattern matching
enum, match, if let
Defining enums
An enum (enumeration) defines a type that can be one of several variants. Unlike enums in many other languages, Rust enums are extremely powerful:
enum Direction {
North,
South,
East,
West,
}
let heading = Direction::North;
match heading {
Direction::North => println!("Going north!"),
Direction::South => println!("Going south!"),
Direction::East => println!("Going east!"),
Direction::West => println!("Going west!"),
}Each variant is accessed with the :: syntax. Enums are often used with match to handle each case.
Enums with data
The real power of Rust enums is that each variant can hold different types and amounts of data:
enum Command {
Quit, // No data
MoveTo(i32, i32), // Two integers
ChangeColor(String), // A String
Write { text: String }, // Named field
}
let commands = vec![
Command::MoveTo(10, 20),
Command::ChangeColor(String::from("#ff0000")),
Command::Quit,
];
for cmd in &commands {
match cmd {
Command::Quit => println!("Quitting!"),
Command::MoveTo(x, y) => println!("Moving to ({x}, {y})"),
Command::ChangeColor(c) => println!("Changing color to red: {c}"),
Command::Write { text } => println!("Writing: {text}"),
}
}This replaces the need for inheritance or union types in other languages. Each variant can be a completely different shape.
The match expression
match must handle every variant of an enum. The compiler enforces exhaustiveness, so you cannot forget a case:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
// Use _ as a catch-all pattern
fn is_small(coin: &Coin) -> bool {
match coin {
Coin::Penny | Coin::Nickel => true,
_ => false, // Everything else
}
}if let syntax
When you only care about one variant, if let is more concise than a full match:
let some_value: Option<i32> = Some(42);
// With match
match some_value {
Some(v) => println!("The value is: {v}"),
None => println!("No value"),
}
// With if let — more concise for single patterns
if let Some(v) = some_value {
println!("The value is: {v}");
} else {
println!("No value");
}Common enums: Option and Result
Two of the most important enums in Rust are built into the standard library:
// Option<T> replaces null in Rust
// enum Option<T> { Some(T), None }
fn find_first_even(numbers: &[i32]) -> Option<i32> {
for &n in numbers {
if n % 2 == 0 {
return Some(n);
}
}
None
}
let nums = [1, 3, 5, 7];
match find_first_even(&nums) {
Some(n) => println!("Found: {n}"),
None => println!("Not a number"),
}// Result<T, E> for operations that can fail
// enum Result<T, E> { Ok(T), Err(E) }
use std::fs;
fn read_config(path: &str) -> Result<String, String> {
fs::read_to_string(path)
.map_err(|e| format!("File not found: {path}"))
}
match read_config("config.toml") {
Ok(content) => println!("Config: {content}"),
Err(e) => println!("{e}"),
}
// The ? operator propagates errors automatically
fn load_config() -> Result<String, std::io::Error> {
let content = fs::read_to_string("config.toml")?;
Ok(content)
}Rust has no null value. Instead, Option forces you to handle the absence of a value explicitly. Result forces you to handle errors. This eliminates entire categories of bugs at compile time.
Your turn
Try running the enums demo with cargo run and cargo check in the terminal below: