Intermediate · Structs, enums and pattern matching · topic 21 of 50 · 30 min
Pattern matching: match, if let and let else
Explanation
Why match?
Enums are only useful if you can act on each variant and get its data out. match does this. It is like a switch statement in other languages, but much stronger: it can look inside data, and it is exhaustive, which means the compiler checks that every possible case is handled.
enum Payment {
Upi(String),
Card { last4: u16 },
Cash,
}
fn describe(p: &Payment) -> String {
match p {
Payment::Upi(id) => format!("UPI from {id}"),
Payment::Card { last4 } => format!("Card ending {last4}"),
Payment::Cash => String::from("Cash"),
}
}A pattern describes the shape of a value. An arm is one pattern plus the code to run. The arms are tried from top to bottom, and the first matching arm runs. match is an expression, so all arms must produce the same type.
Exhaustiveness: the compiler remembers for you
match p {
Payment::Upi(id) => println!("{id}"),
Payment::Cash => println!("cash"),
}
// error[E0004]: non-exhaustive patterns: `&Payment::Card { .. }` not coveredMore pattern tools
fn ticket_price(age: u32) -> u32 {
match age {
0..=4 => 0, // a range
5..=11 => 60,
60 | 61 | 62 => 70, // several values with |
a if a >= 63 => 50, // a guard: an extra condition
_ => 100, // the catch-all
}
}let point = (0, 7);
match point {
(0, y) => println!("on the y axis at {y}"),
(x, 0) => println!("on the x axis at {x}"),
(x, y) => println!("at ({x}, {y})"),
}if let: one interesting case
When you care about only one pattern, a full match with _ => () is long. if let is a shorter form. It runs the block only if the pattern matches. You can add else.
let coupon: Option<u32> = Some(150);
if let Some(off) = coupon {
println!("Rs {off} off");
} else {
println!("No coupon");
}let else: get the value or leave
let else, stable since Rust 1.65, binds a value if the pattern matches; otherwise its else block runs, and that block must leave the current function or loop (return, break, continue or panic). It keeps the "happy path" un-indented.
fn parse_qty(text: &str) -> u32 {
let Ok(n) = text.trim().parse::<u32>() else {
return 0;
};
n
}while let and matches!
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
print!("{top} "); // 3 2 1
}
println!();
let status = 'S';
let is_vowel = matches!(status, 'a' | 'e' | 'i' | 'o' | 'u');
println!("{is_vowel}"); // falseBinding modes in plain words
When you match on a reference, like &Payment, the bound variables are references too (id is a &String). This is why describe could take &Payment without moving anything out. If you match on an owned value, the data moves into the bound variables.
Common mistakes
- Putting _ first: every later arm is unreachable, and the compiler warns. Put specific arms first.
- Different types in different arms: match is an expression, so all arms must give the same type.
- Matching an owned value and then using it again: the data was moved into the pattern. Match on a reference (&value) instead.
- Using if let when two or more cases matter. Use a full match so the compiler can check them all.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Pattern matching: match, if let and let else · 1/7
match p {
Payment::Upi(id) => ...,
Payment::Cash => ...,
}match compares a value against patterns and runs the first arm that fits.
Prefer a video by a teacher? Find free videos on “rust match pattern matching if let let else tutorial” on YouTube ↗
In real life: Order status messages
A food delivery app shows a different message for each order status, and some statuses carry data, like the rider's name or the reason for cancelling. A single match covers every status. When the product team adds a new status such as "Ready for pickup", every screen that forgot to handle it fails to compile, so no user ever sees a blank message.
enum Status {
Placed,
Cooking { minutes_left: u32 },
OutForDelivery(String),
Delivered,
Cancelled { reason: String },
}
fn message(s: &Status) -> String {
match s {
Status::Placed => "Order placed".to_string(),
Status::Cooking { minutes_left } if *minutes_left <= 2 => "Almost ready".to_string(),
Status::Cooking { minutes_left } => format!("Ready in {minutes_left} min"),
Status::OutForDelivery(rider) => format!("{rider} is on the way"),
Status::Delivered => "Enjoy your meal!".to_string(),
Status::Cancelled { reason } => format!("Cancelled: {reason}"),
}
}
fn main() {
println!("{}", message(&Status::Cooking { minutes_left: 12 }));
println!("{}", message(&Status::OutForDelivery(String::from("Imran"))));
}Practice (0/3 done)
What does this print? let n = 7; let kind = match n { 0 => "zero", 1..=5 => "small", x if x % 2 == 0 => "big even", _ => "big odd", }; println!("{kind}");
Why does this not compile? enum Light { Red, Yellow, Green } fn go(l: Light) -> bool { match l { Light::Green => true, Light::Red => false, } }
Write enum Command { Add(u32), Remove(u32), Clear, Show } and fn run(cart: &mut Vec<u32>, cmd: Command) that uses match to change a cart of prices. Then write fn first_even(v: &[i32]) -> Option<i32>, and in main use let else to print "no even number" and return when it gives None.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Methods and associated functionsStructs, enums and pattern matching · same module
- Structs: your own data typesStructs, enums and pattern matching · same module
- Dart 3: enums, records, patterns and sealed classesFlutter and Dart · Null safety and object-oriented Dart
- Records and pattern matchingC# and .NET · Inheritance, interfaces and records
- Decisions: if, else, switch and switch expressionsC# and .NET · Decisions, loops and methods
- break, continue and loop elsePython Programming · Loops: repeating work
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does "exhaustive" mean for match?
Answer: Every possible value must be covered by some arm
The compiler checks that no case is forgotten.
What does the _ pattern do?
Answer: Matches anything and ignores it
_ is the catch-all pattern.
When is if let better than match?
Answer: When you care about only one pattern
if let is a short form for handling a single pattern.
In let Some(x) = opt else { ... };, what must the else block do?
Answer: Leave the current function or loop (return, break, continue or panic)
The else block must diverge, because x would otherwise have no value.
What does matches!(c, 'a'..='z') return?
Answer: true if c is a lowercase ASCII letter, else false
matches! turns a pattern test into a bool.
What does when do in a switch pattern?Related
Answer: Adds a guard condition to a case
The case matches only if the guard is true.
What does when do in a switch arm?Related
Answer: Adds an extra condition the arm must also satisfy
when is a case guard.
A switch case has code but no break. What happens?Related
Answer: Compile error: control cannot fall through
C# does not allow fall-through from a non-empty case (error CS0163).
When does the else block of a for loop run?Related
Answer: Only when the loop finished without a break
Loop else means "no break happened". A break skips it.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Structs, enums and pattern matching”. The module exam comes next.
- Module 5 exam: Structs, enums and pattern matching · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Error handling and collections. 50 topics left in the course before the final exam.