Today, I explored some of the most expressive control flow constructs in Rust. These include match, if let, and let...else. These constructs allow pattern-based control handling with enums and other values — an area where Rust truly shines.
🎯 match – Rust’s Powerful Control Flow Tool
Rust's match is like a switch-case, but safer and more expressive. It lets you compare a value against multiple patterns and execute the corresponding code.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
Each match arm matches a value and executes associated logic. It's exhaustive by default, meaning every possible case must be handled — preventing bugs.
🧩 Patterns That Bind to Values
You can match against values and bind inner data:
enum Coin {
Penny,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}
This is useful for extracting values from enum variants and performing actions with them.
⚙️ Matching with Option
Option is Rust’s way of saying “something or nothing” — a safe alternative to null.
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
Rust ensures that you handle all cases, so it won't compile if you forget the None case.
✅ Exhaustive Matches
Rust’s compiler forces exhaustive matches. If you miss a case (like None in Option), the code won’t compile. This avoids many common runtime errors found in other languages.
🔁 Catch-All Patterns with _
When you want to ignore unmatched patterns, use _:
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
Perfect for default actions or when you only care about a few specific patterns.
📦 Ownership and match
Be mindful: matching an enum with a non-copy type (like String) can move the value unless you borrow it:
let opt = Some(String::from("Hello"));
match &opt {
Some(s) => println!("Found: {}", s),
None => println!("Nothing!"),
}
Matching on &opt lets you borrow and use the value without moving it.
✨ Cleaner Code with if let
When you only care about one pattern (like Some(T)), if let is concise:
if let Some(max) = config_max {
println!("Max is {max}");
}
This saves you from writing boilerplate match arms like _ => ().
🪛 Adding else to if let
Want to handle the unmatched case too? Use else:
if let Coin::Quarter(state) = coin {
println!("State quarter: {state:?}");
} else {
count += 1;
}
🛣️ The “Happy Path” with let...else
let...else lets you focus on the success path, and return early otherwise:
let Coin::Quarter(state) = coin else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old!"))
} else {
Some(format!("{state:?} is new."))
}
This keeps code flat and readable, avoiding nested conditionals.
🧠 Summary
Here’s what I’ve learned on Day 8:
- match is powerful, exhaustive, and pattern-rich.
- It binds values from enums like Option and Result.
- if let and let...else are clean alternatives to match for simple or early-exit cases.
- Catch-all patterns like _ simplify handling defaults.
- Ownership matters in match — borrow if needed to avoid moves.
I now have a stronger grip on Rust’s control flow — it’s expressive, safe, and eliminates many common bugs by design!
Top comments (0)