Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
25: Pattern Matching with match and if let
I've seen this pattern a dozen times in code reviews from developers moving to Rust from Java or Python. They treat Option or Result types like they're just nullable objects, leading to code that feels clunky and fights the compiler.
// A realistic mistake: The "Check-then-Unwrap" pattern
fn print_user_status(user_id: u32, users: &HashMap<u32, User>) {
if users.contains_key(&user_id) {
let user = users.get(&user_id).unwrap();
println!("User {} is active: {}", user.name, user.is_active);
} else {
println!("User not found.");
}
}
The Double-Lookup Tax
At first glance, the code above works. It doesn't crash, and it handles the "not found" case. But it's inefficient. You're asking the HashMap to find the key twice: once for contains_key and once for get. In a tight loop, this is a waste of cycles. More importantly, calling .unwrap() is a habit that eventually leads to a panic in production when you forget a check somewhere. You're essentially telling the compiler, "Trust me, I already checked this," which defeats the purpose of Rust's safety guarantees.
Solving for Exhaustiveness with match
The "Rust way" is to embrace the fact that get() returns an Option. Instead of checking if the value exists and then grabbing it, we do both in one motion using match. This forces us to handle every possible outcome—what we call "exhaustive matching."
fn print_user_status(user_id: u32, users: &HashMap<u32, User>) {
match users.get(&user_id) {
Some(user) => println!("User {} is active: {}", user.name, user.is_active),
None => println!("User not found."),
}
}
Notice how much cleaner this is. We've collapsed the check and the retrieval into a single operation. If I were to add a new variant to the return type (though Option is fixed), the compiler would scream at me until I handled it. That's the safety net I rely on every day.
Trimming the Noise with if let
Now, match is powerful, but it can feel like overkill. Imagine you're writing a function where you only care if the user exists, and if they don't, you just want to silently return or do nothing. Writing None => () feels like boilerplate noise.
That's where if let comes in. It's essentially syntactic sugar for a match that only cares about one specific pattern.
fn notify_admin(user_id: u32, users: &HashMap<u32, User>) {
// We only care about the Some case; the None case is implicitly ignored
if let Some(user) = users.get(&user_id) {
send_alert(&user.email, "A user has logged in.");
}
}
I usually follow a simple rule of thumb: use match when you have two or more distinct paths of logic that must be handled. Use if let when you're essentially saying, "If this is the case, do this thing; otherwise, just keep moving." It keeps the indentation shallow and the intent clear.
📋 Practical Task
Implementing a Game Command Processor
You are building a simple text-adventure game. You have an enum representing possible player commands, and some of those commands carry additional data (like the item name the player wants to pick up).
Your Task: Create a function process_command that takes a Command enum. Use a match statement to handle the following logic:
Command::Move(direction): Print "Moving to the [direction]!".Command::PickUp(item): Print "You picked up the [item]!".Command::Quit: Print "Goodbye!".Command::Wait: Print "You wait patiently...".
Additionally, write a separate snippet using if let that specifically checks if the command is PickUp. If it is, print "Inventory updated!", otherwise do nothing.
enum Command {
Move(String),
PickUp(String),
Quit,
Wait,
}
fn process_command(cmd: Command) {
// Your match implementation here
}
fn main() {
let my_cmd = Command::PickUp(String::from("Rusty Sword"));
process_command(my_cmd);
// Your if let implementation here
}There are no comments for now.