An enum defines a type with a fixed set of named variants, and unlike C-style enums, each Rust variant can carry its own associated data of any shape. match compares a value against a series of patterns and runs the code for the first match; critically, the compiler requires every possible variant to be handled (or an explicit _ catch-all), so a newly added enum variant that isn't handled somewhere is a compile error, not a silently-skipped runtime bug.
Enums with data per variant
Each variant can carry different data — a fundamentally more expressive tool than C's plain-integer enums.
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields, like a struct
Write(String), // one value
ChangeColor(i32, i32, i32), // a tuple of values
}
let msg = Message::Move { x: 10, y: 20 };Option<T>: Rust's answer to null
Rust has no null value. Instead, the standard library defines Option<T> as an ordinary enum with two variants: Some(T) holds a value, None holds nothing. Because it's a distinct type from T, the compiler forces you to explicitly handle the "nothing" case before you can use the inner value — there's no way to accidentally dereference a null pointer.
// The actual standard library definition:
// enum Option<T> { Some(T), None }
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Alice"))
} else {
None
}
}
match find_user(1) {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}match must be exhaustive; if let for the simple case
If a match doesn't cover every variant, it's a compile error — this is what makes match a safety feature, not just a switch statement. For the common case of only caring about one variant, if let is a shorter alternative to a full match.
let config_value: Option<u8> = Some(3);
// full match: handles both cases
match config_value {
Some(v) => println!("value is {}", v),
None => println!("no value"),
}
// if let: shorter, only cares about the Some case
if let Some(v) = config_value {
println!("value is {}", v);
}