Hello!
In our last lesson, we dove into the Option<T> enum, Rust's robust solution for handling values that might be absent. We saw how it forces us to deal with both the Some(value) and None cases at compile time, eliminating a whole category of null-related bugs. We also got a small preview of the match keyword.
Today, we're going to master the powerful mechanism behind match: pattern matching. This is one of Rust's most beloved features. It allows you to control your program's flow by comparing a value against the "shape" or structure of your data. We'll explore the exhaustive match expression and its more concise cousin, if let. Understanding these tools is absolutely critical for writing idiomatic Rust, and they are used extensively in Solana programs to handle instructions, validate accounts, and manage state.
What are Patterns?
Before we dive into match, let's quickly define what a "pattern" is in Rust. A pattern is a combination of literals, variable names, wildcards, and destructuring syntax that describes the structure of a type. For example, Some(x) is a pattern that matches the Some variant of an Option and binds its inner value to a new variable x.
The "Let's Get Rusty" channel has an excellent video that introduces patterns and where you can find them in Rust.
To start, let's get a high-level overview of what patterns are and the different places they can be used in Rust.
Watch the introduction from 00:10 to 00:37. This will give you a quick summary of the components that make up a pattern.
As you saw, patterns appear everywhere, from let statements to function parameters. Today, we'll focus on their use in control flow constructs.
The match Expression: Exhaustive Pattern Matching
The match expression is Rust's primary tool for pattern matching. Think of it like a coin-sorting machine: a value goes in, and it falls into the first slot (or "arm") whose pattern it fits.
The most important feature of match is that it is exhaustive. The Rust compiler will check to make sure you have a pattern for every possible value of the type you are matching on. This is what makes it so safe.
The match Control Flow Construct
The Rust Book provides a fantastic introduction to match, explaining its core concepts, the importance of exhaustiveness, and how to bind values from within a pattern.
Please read the following sections from the chapter on match: "The match Control Flow Construct": Focus on the coin-sorting analogy and the basic structure of a match expression. "Patterns That Bind to Values": This is key. Understand how the Coin::Quarter(state) pattern allows you to extract the UsState value. "Matches Are Exhaustive": Pay close attention to the compiler error. This is the safety net that prevents you from forgetting cases like None. "Catch-All Patterns and the _ Placeholder": Learn how to handle all remaining cases using a variable name or the _ wildcard.
Let's recap the key points from that reading:
- A
matchexpression compares a value against a series of patterns. - Each pattern is followed by
=>and an expression to execute. - The compiler guarantees exhaustiveness, forcing you to handle all possible cases.
- You can use a variable name (e.g.,
other) as a catch-all pattern to bind the value, or_to ignore it. - Patterns can destructure enums, structs, and tuples, binding their contents to variables.
Let's see this in action with a struct.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x: 0, y: 0 } => println!("On the origin"),
Point { x, y: 0 } => println!("On the x-axis at {}", x),
Point { x: 0, y } => println!("On the y-axis at {}", y),
Point { x, y } => println!("At coordinates ({}, {})", x, y),
}
}
// Output: On the y-axis at 7
Notice how we can match on specific literal values (x: 0) or simply bind the field's value to a new variable (y). The final arm Point { x, y } acts as a catch-all for any Point that didn't match the more specific patterns above it.
Test your understanding!
You have an enum representing a user action in a web application.
enum WebEvent {
PageLoad,
PageUnload,
KeyPress(char),
Click { x: i64, y: i64 },
}
Write a function inspect_event(event: WebEvent) that uses a match expression to print a different message for each event variant. For KeyPress, it should print the character, and for Click, it should print the coordinates.
Show answer
fn inspect_event(event: WebEvent) {
match event {
WebEvent::PageLoad => println!("Page loaded"),
WebEvent::PageUnload => println!("Page unloaded"),
WebEvent::KeyPress(c) => println!("Pressed key: '{}'", c),
WebEvent::Click { x, y } => println!("Clicked at: x={}, y={}", x, y),
}
}
This demonstrates matching on simple variants (PageLoad), destructuring a tuple-like variant (KeyPress(c)), and destructuring a struct-like variant (Click { x, y }).
Advanced Patterns and if let
While match is powerful, sometimes it's more verbose than you need. If you only care about matching one specific pattern and want to ignore everything else, Rust provides a more concise syntax: if let.
Let's watch a video that covers both if let and some more advanced match patterns.
The "Patterns and Matching" video we saw earlier also provides a great walkthrough of if let and other pattern types. It's a great way to see these concepts in code.
Watch the following segments: Patterns in Match Expressions (00:55 - 02:38): This is a great visual recap of what we just read about match, including exhaustiveness and catch-all patterns. Patterns in If Let Expressions (02:38 - 04:49): Focus on how if let provides a more concise way to handle a single case you care about. Note that it is not exhaustive. Patterns in For Loops (05:31 - 06:02): This is a quick but useful example of how for loops use patterns to destructure values from an iterator, like the (index, value) tuple from .enumerate().
Let's contrast match and if let. Imagine we have an Option<i32> and only want to do something if it's Some(3).
Using match:
let favorite_color: Option<i32> = Some(7);
match favorite_color {
Some(3) => println!("Three is my favorite color!"),
_ => { // We have to handle all other cases
// Do nothing
}
}
Using if let:
let favorite_color: Option<i32> = Some(7);
if let Some(3) = favorite_color {
println!("Three is my favorite color!");
}
// No `else` is required if you don't need to handle other cases.
The if let version is clearly more direct for this scenario. You can think of if let as syntax sugar for a match that only has one interesting arm.
More Expressive Patterns: Guards and Ranges
You can make your patterns even more powerful with a few extra tools. The Tensor Programming channel has a slightly older but very clear video showing some of these.
Intro to Rustlang (Control Flow, Conditionals and Pattern Matching)
This video introduces a few advanced patterns that give you more control within a match arm, such as matching ranges and adding extra conditions.
Please watch these two short clips: Advanced Match Patterns (09:09 - 10:07): See how you can use | to match multiple values in one arm, and ..= to match a range of values. Match Guards (10:58 - 11:51): Learn how to add an if condition to a pattern for more complex logic.
Here’s an example combining these concepts:
let num = 10;
match num {
1 => println!("One!"),
2 | 3 | 5 | 7 => println!("A small prime"),
x if x % 2 == 0 => println!("An even number: {}", x), // Match guard
10..=20 => println!("A number between 10 and 20"), // Range
_ => println!("Something else"),
}
In this case, 10 matches the pattern x if x % 2 == 0 first, so it prints "An even number: 10". The order of match arms is very important!
Test your understanding!
You are given a variable value: Option<i32>. You need to print "Found a positive number!" only if the Option is Some and the number inside is greater than 0. Which construct is more appropriate here, match or if let? Write the code to implement this check.
Show answer
if let is more appropriate because we only care about one specific case (Some with a positive number) and want to ignore all others (None and Some with a non-positive number).
let value: Option<i32> = Some(10);
if let Some(number) = value {
if number > 0 {
println!("Found a positive number!");
}
}
Or, even more idiomatically, using a match guard directly with if let:
let value: Option<i32> = Some(10);
if let Some(number) = value && number > 0 {
println!("Found a positive number!");
}
This shows how if let can be combined with other conditions for very expressive, concise code. Note that match guards with if let are a more recent addition to Rust, but very useful!
Conclusion
Pattern matching is a cornerstone of expressive and safe Rust programming. It allows you to write clean, readable logic that is enforced by the compiler.
Let's summarize the key takeaways:
- Patterns are used to describe the shape of data.
- The
matchkeyword provides exhaustive, powerful control flow based on patterns. It forces you to handle every possibility, preventing bugs. if letis a concise, non-exhaustive alternative for when you only care about a single pattern.- Patterns can destructure enums, structs, and tuples to bind their inner values to variables.
- You can make patterns more specific with ranges (
..=), multiple options (|), and conditional guards (if ...).
You now have a solid foundation in using enums, Option, and pattern matching. In our next lesson, we will introduce another crucial enum from the standard library: Result<T, E>. This is Rust's primary tool for recoverable error handling. You'll see immediately how the pattern matching skills you learned today are essential for dealing with the Ok(T) and Err(E) variants of Result.
Can't find a good explanation? Sign up and we'll make it for you
Sign up