Rust Fundamentals: Ownership, Errors and Fearless Concurrency · Rust Basics and Ownership
Enums, Option and Pattern Matching
Enums model 'one of several possibilities', each variant optionally carrying data. Option replaces null, match must handle every case, and if let handles just one.
What you'll build
A shape calculator and a user lookup: an enum with variants that hold different data, match to handle every variant, and Option for "might not exist" without null pointers.
The idea
An enum is a type that is exactly one of several variants, and each variant can carry its own data:
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle(f64, f64, f64),
}
match compares a value against patterns and runs the first that fits. It must be exhaustive: forget a variant and the code doesn't compile. Patterns can destructure data, bind variables, use ranges (1..=9), alternatives ('a' | 'e'), guards (if x > 10), and _ for "anything else".
Rust has no null. A value that might be missing is an Option<T>: either Some(value) or None. Because it's a different type from T, the compiler forces you to handle the None case.
if let Some(x) = maybe { ... } handles one pattern when you don't care about the rest.
The code
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle(f64, f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0; // Heron's formula
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
fn find_user(id: u32) -> Option<&'static str> {
match id {
1 => Some("ana"),
2 => Some("ben"),
_ => None,
}
}
fn describe(n: i32) -> &'static str {
match n {
i32::MIN..=-1 => "negative",
0 => "zero",
1..=9 => "a single digit",
x if x % 2 == 0 => "a big even number",
_ => "a big odd number",
}
}
fn main() {
let shapes = [
Shape::Circle { radius: 1.5 },
Shape::Rectangle { width: 3.0, height: 4.0 },
Shape::Triangle(3.0, 4.0, 5.0),
];
for s in &shapes {
println!("area: {:.2}", area(s));
}
for id in [1, 3] {
match find_user(id) {
Some(name) => println!("user {id}: {name}"),
None => println!("user {id}: not found"),
}
}
if let Some(name) = find_user(2) {
println!("if let found {name}");
}
let name = find_user(9).unwrap_or("guest"); // a default for None
println!("fallback: {name}");
for n in [-4, 0, 7, 42, 99] {
println!("{n} is {}", describe(n));
}
}
Output
area: 7.07 area: 12.00 area: 6.00 user 1: ana user 3: not found if let found ben fallback: guest -4 is negative 0 is zero 7 is a single digit 42 is a big even number 99 is a big odd number
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Using
_too early in a match, hiding variants you add later; list variants explicitly when practical. unwrap()onOptionin real code. It panics onNone; usematch,if let,unwrap_oror?.- Modelling states with several booleans (
is_loading,is_error) instead of one enum; enums make impossible states unrepresentable. - Forgetting that match arms must return the same type.
Key points
- Enums: one of several variants, each with optional data.
matchis exhaustive and destructures data; guards and ranges add precision.Option<T>(Some/None) replaces null and must be handled.if letfor a single pattern;unwrap_orfor defaults.
Further reading: Enums and pattern matching.
Practice questions
Why doesn't Rust have null, and what replaces it?
Answer
Null references cause crashes when code forgets to check them. Rust uses Option<T> (Some(value) or None), a distinct type that the compiler forces you to handle before using the value.
What does it mean that match is exhaustive?
Answer
Every possible value must be covered by some arm; if an enum variant (or range of values) is missing, the code doesn't compile. A wildcard _ can cover the rest.
Open in the app to mark lessons complete, take notes and track your progress.