Lesson 8 of 94
Control Flow: if, loop, while, for & match
Rust's control flow will feel familiar, with three twists: if and loop produce values, for loops only iterate over things (there is no C-style for (int i = 0; ...)), and match replaces switch with something far more powerful.
| Concept | Rust |
|---|---|
| Condition | if x > 0 { ... } (no parentheses) |
| Conditional value | if cond { a } else { b } |
| Infinite loop | loop { ... } |
| Counting loop | for i in 0..n |
| Inclusive counting loop | for i in 1..=n |
| Labeled break | 'outer: for ... { break 'outer; } |
| Multi-way branch | match |
1. if and else
Parentheses around the condition are not needed (the compiler warns if you add them), but the braces are always required, even for one line.
fn main() {
let balance = 120;
let price = 80;
if balance >= price {
println!("Purchase approved");
} else {
println!("Insufficient funds");
}
}Purchase approved2. else if chains
fn main() {
let cpu_percent = 87;
if cpu_percent >= 95 {
println!("CRITICAL");
} else if cpu_percent >= 80 {
println!("WARNING");
} else {
println!("OK");
}
}WARNING3. if as a value
if is an expression, so you can assign its result. This replaces the ternary operator found in other languages.
fn main() {
let items_in_cart = 1;
let label = if items_in_cart == 1 { "item" } else { "items" };
println!("You have {items_in_cart} {label}");
}You have 1 item4. Both branches must have the same type
The value of an if must have one type, so every branch must produce that type.
fn main() {
let in_stock = true;
let stock_label = if in_stock { "available" } else { 0 };
println!("{stock_label}");
}error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:3:58
|
3 | let stock_label = if in_stock { "available" } else { 0 };
| ----------- ^ expected `&str`, found integer
| |
| expected because of this5. An if without else produces ()
If there is no else, the if produces () when the condition is false, so it cannot be used as a value of another type.
fn main() {
let score = 72;
let grade = if score >= 50 { "pass" };
println!("{grade}");
}error[E0317]: `if` may be missing an `else` clause
--> src/main.rs:3:17
|
3 | let grade = if score >= 50 { "pass" };
| ^^^^^^^^^^^^^^^^^------^^
| | |
| | found here
| expected `&str`, found `()`
|
= note: `if` expressions without `else` evaluate to `()`
= help: consider adding an `else` block that evaluates to the expected type6. loop: repeat until break
loop repeats forever until you break. It is clearer than while (true) and the compiler understands it better.
fn main() {
let mut attempts = 0;
loop {
attempts += 1;
println!("attempt {attempts}");
if attempts == 3 {
break;
}
}
println!("done after {attempts} attempts");
}attempt 1
attempt 2
attempt 3
done after 3 attempts7. break with a value
A loop can return a value through break value. This is handy for "retry until it works" logic.
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter * counter > 50 {
break counter;
}
};
println!("first number whose square exceeds 50: {result}");
}first number whose square exceeds 50: 88. while loops
fn main() {
let mut remaining = 3;
while remaining > 0 {
println!("{remaining}...");
remaining -= 1;
}
println!("Liftoff!");
}3...
2...
1...
Liftoff!9. for over a range
0..5 is a range from 0 up to, but not including, 5. 1..=5 includes the end.
fn main() {
for i in 0..5 {
print!("{i} ");
}
println!();
for i in 1..=5 {
print!("{i} ");
}
println!();
}0 1 2 3 4
1 2 3 4 5for loops cannot go out of bounds or loop forever by accident with a wrong i++, which is why Rust has no C-style for.
10. Counting down and stepping
.rev() reverses a range, and .step_by(n) takes every nth value.
fn main() {
for i in (1..=5).rev() {
print!("{i} ");
}
println!();
for minute in (0..60).step_by(15) {
print!(":{minute:02} ");
}
println!();
}5 4 3 2 1
:00 :15 :30 :4511. Summing with a for loop
fn main() {
let mut total = 0;
for n in 1..=100 {
total += n;
}
println!("1 + 2 + ... + 100 = {total}");
}1 + 2 + ... + 100 = 505012. continue skips to the next iteration
fn main() {
for order_id in 1..=8 {
if order_id % 3 == 0 {
continue; // every third order is on hold
}
print!("#{order_id} ");
}
println!();
}#1 #2 #4 #5 #7 #813. Nested loops
fn main() {
for row in 1..=4 {
for col in 1..=4 {
print!("{:4}", row * col);
}
println!();
}
} 1 2 3 4
2 4 6 8
3 6 9 12
4 8 12 1614. Labeled break out of nested loops
Rust can label a loop and break out of it from an inner loop. The label starts with a single quote: 'outer.
fn main() {
let target = 42;
'search: for a in 1..=10 {
for b in 1..=10 {
if a * b == target {
println!("found {a} x {b} = {target}");
break 'search;
}
}
}
println!("search finished");
}found 6 x 7 = 42
search finished15. Labeled continue
fn main() {
'rows: for row in 1..=3 {
for col in 1..=3 {
if col > row {
println!();
continue 'rows;
}
print!("({row},{col}) ");
}
println!();
}
}(1,1)
(2,1) (2,2)
(3,1) (3,2) (3,3)16. break with a value from a labeled loop
fn main() {
let grid_size = 5;
let first_diagonal_multiple_of_7 = 'outer: loop {
for i in 1..=grid_size {
for j in 1..=grid_size {
if i == j && (i * j) % 7 == 0 {
break 'outer Some(i);
}
}
}
break None;
};
println!("{:?}", first_diagonal_multiple_of_7);
}NoneSome(i) and None are the two forms of an Option: "a value" or "no value". Here no diagonal cell is a multiple of 7, so the result is None. Option covers this in detail.
17. match: Rust's switch
match compares a value against patterns from top to bottom and runs the first arm that matches. _ matches anything. Arms never fall through, so there is no break.
fn main() {
let day_number = 6;
match day_number {
1 => println!("Monday"),
2 => println!("Tuesday"),
3 => println!("Wednesday"),
4 => println!("Thursday"),
5 => println!("Friday"),
6 | 7 => println!("Weekend"),
_ => println!("Not a day"),
}
}Weekend6 | 7 matches either value.
18. match must cover every possible value
This is called exhaustiveness. The compiler checks that no value can slip through. An i32 has about four billion possible values, so without _ this does not compile.
fn main() {
let status = 2;
match status {
1 => println!("active"),
2 => println!("suspended"),
}
}error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
--> src/main.rs:3:11
|
3 | match status {
| ^^^^^^ patterns `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
|
= note: the matched value is of type `i32`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms
|
5 ~ 2 => println!("suspended"),
6 ~ i32::MIN..=0_i32 | 3_i32..=i32::MAX => todo!(),
|Exhaustiveness becomes very powerful with enums. When you add a new enum variant, every match that forgets it stops compiling. See Enums.
19. match produces a value
fn main() {
let http_status = 404;
let message = match http_status {
200 => "OK",
301 => "Moved Permanently",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Unknown",
};
println!("{http_status} {message}");
}404 Not Found20. Matching ranges
a..=b in a pattern matches every value from a to b inclusive.
fn main() {
let age = 34;
let group = match age {
0..=12 => "child",
13..=17 => "teenager",
18..=64 => "adult",
_ => "senior",
};
println!("{age} is {group}");
}34 is adult21. Match guards
An if after a pattern adds an extra condition, called a guard. The pattern n matches any value and names it n.
fn main() {
for balance in [-50, 0, 20, 5_000] {
let status = match balance {
n if n < 0 => "overdrawn",
0 => "empty",
n if n < 100 => "low",
_ => "healthy",
};
println!("{balance:>5}: {status}");
}
} -50: overdrawn
0: empty
20: low
5000: healthy[-50, 0, 20, 5_000] is an array. A for loop can go through it just like a range. Arrays are the topic of the next lesson.
22. Realistic example: FizzBuzz with match
Matching on two values at once, (n % 3, n % 5), makes the four cases very clear. The pair is a tuple, covered in the next lesson.
fn main() {
for n in 1..=15 {
match (n % 3, n % 5) {
(0, 0) => println!("FizzBuzz"),
(0, _) => println!("Fizz"),
(_, 0) => println!("Buzz"),
_ => println!("{n}"),
}
}
}1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz23. Realistic example: finding prime numbers
fn main() {
let mut primes_found = 0;
'candidates: for n in 2..=50 {
let mut divisor = 2;
while divisor * divisor <= n {
if n % divisor == 0 {
continue 'candidates;
}
divisor += 1;
}
print!("{n} ");
primes_found += 1;
}
println!();
println!("{primes_found} primes up to 50");
}2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
15 primes up to 5024. Realistic example: retry with exponential backoff
This simulates a flaky service that succeeds on the fourth call. loop with break value returns the result, and the delay doubles each time up to a limit.
const MAX_ATTEMPTS: u32 = 5;
const MAX_DELAY_MS: u32 = 1_000;
fn call_service(attempt: u32) -> bool {
attempt >= 4 // pretend the service recovers on the 4th try
}
fn main() {
let mut attempt = 0;
let mut delay_ms = 100;
let succeeded = loop {
attempt += 1;
if call_service(attempt) {
break true;
}
if attempt == MAX_ATTEMPTS {
break false;
}
println!("attempt {attempt} failed, retrying in {delay_ms} ms");
delay_ms = (delay_ms * 2).min(MAX_DELAY_MS);
};
println!("succeeded: {succeeded} after {attempt} attempt(s)");
}attempt 1 failed, retrying in 100 ms
attempt 2 failed, retrying in 200 ms
attempt 3 failed, retrying in 400 ms
succeeded: true after 4 attempt(s)25. Realistic example: savings growth with while
How many years until a deposit doubles at 7% interest per year?
fn main() {
let start = 10_000.0;
let rate = 0.07;
let mut balance = start;
let mut years = 0;
while balance < start * 2.0 {
balance *= 1.0 + rate;
years += 1;
if years % 3 == 0 {
println!("year {years:2}: {balance:.2}");
}
}
println!("Doubled after {years} years: {balance:.2}");
}year 3: 12250.43
year 6: 15007.30
year 9: 18384.59
Doubled after 11 years: 21048.5226. Realistic example: the Collatz sequence
Each step halves an even number or computes 3n + 1 for an odd one. The if expression picks the next value.
fn main() {
let mut n: u64 = 27;
let mut steps = 0;
let mut highest = n;
while n != 1 {
n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
highest = highest.max(n);
steps += 1;
}
println!("27 reaches 1 after {steps} steps, peaking at {highest}");
}27 reaches 1 after 111 steps, peaking at 923227. Realistic example: classifying HTTP responses
A web client often decides what to do based on the status code's range. The match covers every possible u16 value, so the compiler guarantees that no status is forgotten.
fn action_for(status: u16) -> &'static str {
match status {
100..=199 => "informational: keep waiting",
200 | 201 | 204 => "success: process body",
200..=299 => "success: unusual code, log it",
301 | 302 | 307 | 308 => "redirect: follow Location header",
300..=399 => "redirect: unsupported",
401 | 403 => "auth problem: refresh token",
429 => "rate limited: back off",
400..=499 => "client error: do not retry",
500..=599 => "server error: retry later",
_ => "invalid status code",
}
}
fn main() {
for status in [200, 206, 302, 401, 429, 404, 503, 999] {
println!("{status}: {}", action_for(status));
}
}200: success: process body
206: success: unusual code, log it
302: redirect: follow Location header
401: auth problem: refresh token
429: rate limited: back off
404: client error: do not retry
503: server error: retry later
999: invalid status codeArms are checked from top to bottom, so 200 | 201 | 204 wins over the wider 200..=299 that follows it.
Summary
ifneeds no parentheses but always needs braces. It is an expression, so it replaces the ternary operator.looprepeats untilbreak, andbreak valuereturns a value from it.whilerepeats while a condition holds.foriterates over ranges (0..n,1..=n) and collections.- Labels (
'outer) letbreakandcontinuetarget an outer loop. matchreplacesswitch: no fall-through, patterns with|, ranges, and guards, and the compiler checks that every case is covered.
Next: Tuples & Arrays.
