Control Flow and Pattern Matching
Learning Objectives
- You can use if expressions to make decisions in your code.
- You understand the different types of loops in Rust and when to use each.
- You can use match expressions for powerful pattern matching.
- You know when to use if let as a shortcut for simple patterns.
- You can build a complete program using control flow structures.
Warming up
Here is a set of tiny programming tasks to get you familiar with syntax that relates to control flow and pattern matching. You can solve these exercises directly in the embedded code editor.
If Expressions
We’ve already seen if in previous examples, but let’s look at it more carefully. In Rust, if is an expression, not just a statement. This means it returns a value.
Basic If/Else
Here’s the basic form:
The condition in an if must be a boolean (true or false). Unlike some languages, Rust will not automatically convert numbers to booleans:
let number = 3;
if number { // Error! Expected bool, found integer
println!("This won't work");
}
You must explicitly compare:
If as an Expression
Since if is an expression, you can use it to assign values:
Notice a few important things:
- Both branches must return the same type (both are
f64here) - We don’t use semicolons after the values in each branch (they’re expressions, not statements)
- We do need a semicolon after the closing brace because the entire
ifexpression is part of aletstatement
Note: Rust has no ternary operator (
condition ? value1 : value2) becauseifexpressions serve the same purpose and are more readable.
Loops
Rust has three types of loops: loop, while, and for. Each serves a different purpose.
The loop Keyword
The loop keyword creates an infinite loop. You exit it with break:
You can also return values from loops using break:
While Loops
Rust has also while loops for condition-based looping. However, in most cases, for loops are preferred for iteration because they’re safer and more concise. Here’s a quick example of a while loop:
fn main() {
let mut cups = 1;
while cups <= 5 {
println!("Cup #{}", cups);
cups += 1;
}
} For Loops
The for loop is used to iterate over collections or ranges. This is the loop you’ll use most often:
Note the range syntax:
1..6is a range from 1 to 5 (excludes 6)1..=5is a range from 1 to 5 (includes 5)
You can iterate in reverse using .rev():
Note:
forloops are preferred overwhileloops for iteration because they’re safer. With aforloop, you can’t accidentally create an infinite loop or access an array out of bounds.
Loop Labels
When you have nested loops, you can label them and break or continue to a specific loop:
Loop labels start with a single quote (') and are useful when you need to break out of nested loops.
Match Expression
The match expression is one of Rust’s most powerful features. It allows you to compare a value against a series of patterns and execute code based on which pattern matches.
Basic Match
Let’s start with a simple example:
The _ is a wildcard that matches anything. Think of it as the “default” case.
Note: Match expressions must be exhaustive - they must cover all possible values. If you remove the
_pattern and the value is 4, the code won’t compile!
Match as an Expression
Like if, match is an expression and returns a value:
Matching Multiple Patterns
You can match multiple values with | (or):
Matching Ranges
You can match ranges of values:
Note: The
&'staticlifetime indicates that the returned string slice has a static lifetime, meaning it lives for the entire duration of the program. This is used as we’re returning string literals.
Match Guards
You can add if conditions to match arms:
Matching Strings
You can match on strings and string slices:
If Let: A Shortcut
Sometimes you only care about one specific pattern and want to ignore all others. The if let syntax provides a convenient shortcut:
Note: Don’t worry about
SomeandNonefor now - we’ll look into theOptiontype in the next chapter. The key point is thatif letis useful when you only care about one pattern.
If Let with Else
You can combine if let with else:
When to Use If Let vs Match
Use if let when:
- You only care about one pattern
- You want more concise code for simple cases
Use match when:
- You need to handle multiple patterns
- You want the compiler to check exhaustiveness
- Your logic is complex
Example: Coffee Shop Menu System
Let’s build a complete menu system that demonstrates all the control flow concepts we’ve learned:
Let’s break down the key control flow elements in this program:
1. Main Loop
The program uses an infinite loop that only exits when the user chooses to quit:
loop {
display_menu();
let input = read_number("Enter choice: ");
// ... process input
if user_wants_to_quit {
break;
}
}
2. Menu Choice Handling
The main menu logic uses match with ranges and specific values:
match choice {
0 => /* Exit */,
1..=4 => /* Add drink */,
_ => /* Invalid choice */,
}
3. Utility Functions
Functions like get_drink_name and get_drink_price use match to return values based on the drink choice. This keeps the main logic clean and focused.
Common Patterns and Idioms
Let’s look at some common patterns you’ll use frequently:
Looping with Index
When you need both the index and the value:
Early Return Pattern
Exit early when conditions aren’t met:
Counting with Conditions
Finding Maximum (and Minimum)
Summary
In this chapter, we explored Rust’s control flow structures:
- If expressions can be used as statements or expressions to return values
- Rust has no ternary operator because if expressions serve the same purpose
- Three types of loops:
loopfor infinite loops,whilefor conditional loops, andforfor iteration - For loops are preferred for iteration because they’re safer than while loops
- Match expressions provide powerful pattern matching with exhaustiveness checking
- Match can use: ranges, multiple patterns with
|, and guards withif - If let provides a convenient shortcut when you only care about one pattern
- All match arms must return the same type when match is used as an expression
The match expression is particularly powerful, enabling concise and clear handling of complex branching logic.