Flow control determines the path your program takes through its instructions. Rust's flow control constructs are largely familiar to developers from other languages, but Rust adds several key differences: if is an expression that returns a value, loop is a dedicated keyword for infinite loops, and match is a full pattern-matching system that the compiler enforces exhaustively.
The `if` Statement
Rust's if works like most languages, but without parentheses around the condition:
fn if_statement() {
let temp = 35;
if temp > 30 {
println!("really hot outside!");
} else if temp < 10 {
println!("really cold!");
} else {
println!("temperature is OK");
}
}
Braces {} are mandatory in Rust, even for single-statement bodies. This is a deliberate design choice to eliminate a class of bugs caused by misleading indentation (the "dangling else" problem in C).
`if` as an Expression
In Rust, if is an expression, not just a statement. This means it evaluates to a value and can be used on the right-hand side of a variable assignment:
let temp = 35;
// if as an expression
let day = if temp > 20 { "sunny" } else { "cloudy" };
println!("today is {}", day); // today is sunny
This eliminates the need for the ternary operator (condition ? a : b) that exists in C, C++, and Java. In Rust, if itself serves that role.
For multi-branch expressions:
let description = if temp > 20 { "hot" } else if temp < 10 { "cold" } else { "OK" };
println!("it is {}", description);
And nested if expressions:
let label = if temp > 20 {
if temp > 30 { "very hot" } else { "hot" }
} else if temp < 10 {
"cold"
} else {
"OK"
};
println!("it is {}", label);
Important: When if is used as an expression, every branch must return the same type. The compiler enforces this.
The `while` Loop
The while loop executes a block repeatedly as long as a condition is true:
fn while_loop_example() {
let mut x = 1;
while x < 1000 {
x *= 2;
if x == 64 { continue; } // skip the rest of this iteration
println!("x is {}", x);
}
}
continueskips to the next iterationbreakexits the loop immediately
Execution Flow:
x=1 -> x=2 -> x=4 -> x=8 -> x=16 -> x=32 -> x=64 (skip) -> x=128 -> ... -> x=1024 (exits)
The `loop` Keyword
Rust provides a dedicated loop keyword for infinite loops. This is semantically cleaner than while true {} and enables the compiler to reason better about the code:
fn loop_example() {
let mut y = 1;
loop {
y *= 2;
println!("y = {}", y);
if y == 1 << 10 { break; } // break when y reaches 1024
}
}
`loop` as an Expression
Like if, loop can also return a value using break with a value:
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // returns 20 from the loop
}
};
println!("result = {}", result); // result = 20
This is unique to Rust and enables patterns where you need to keep trying an operation until it succeeds, and then return the result.
The `for` Loop
Rust's for loop iterates over ranges and iterators. It is the preferred loop in Rust because it is safer (no off-by-one errors) and more expressive:
fn for_loop_example() {
// Range: 1..11 means 1 inclusive to 11 exclusive (1, 2, ..., 10)
for x in 1..11 {
if x == 3 { continue; } // skip 3
if x == 8 { break; } // stop at 8
println!("x = {}", x);
}
}
Output: 1, 2, 4, 5, 6, 7 (skips 3, stops before 8)
Range Types
| Syntax | Meaning |
|---|---|
1..10 | 1 to 9 (exclusive end) |
1..=10 | 1 to 10 (inclusive end) |
..10 | from start to 9 |
1.. | from 1 to end |
Iterating with Enumeration
Use .enumerate() to get both the index and the value:
fn enumerate_example() {
for (pos, y) in (30..=40).enumerate() {
println!("{}: {}", pos, y);
}
// Output:
// 0: 30
// 1: 31
// ...
// 10: 40
}
Iterating Over Collections
let fruits = vec!["apple", "banana", "cherry"];
for fruit in &fruits {
println!("{}", fruit);
}
// With index:
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
The `match` Statement
match is Rust's most powerful control flow construct. It is like a switch statement, but exhaustive, expressive, and safe:
fn match_example() {
let country_code = 44;
let country = match country_code {
44 => "UK",
46 => "Sweden",
7 => "Russia",
1..=999 => "unknown", // inclusive range pattern
_ => "invalid" // default case (catches everything else)
};
println!("country code {} is {}", country_code, country);
// country code 44 is UK
}
The _ wildcard pattern matches any value not matched by the previous arms. The Rust compiler enforces that your match is exhaustive — you must cover all possible values (or use _).
Match with Multiple Patterns
let x = 5;
let description = match x {
1 | 2 => "one or two", // OR pattern
3..=5 => "three to five", // range pattern
_ => "other"
};
println!("{}", description); // three to five
Match as an Expression
Like if, match is also an expression that returns a value:
let score = 85;
let grade = match score {
90..=100 => 'A',
80..=89 => 'B',
70..=79 => 'C',
60..=69 => 'D',
_ => 'F',
};
println!("Grade: {}", grade); // Grade: B
Match with Guards
You can add conditions to match arms using if guards:
let x = 7;
let description = match x {
n if n % 2 == 0 => "even",
n if n % 2 != 0 => "odd",
_ => "unknown" // needed to satisfy exhaustiveness
};
println!("{} is {}", x, description); // 7 is odd
Match on Enums and Tuples
match is especially powerful when used with enums and tuples (covered in the data structures post):
let point = (0, 5);
match point {
(0, 0) => println!("origin"),
(0, y) => println!("on y-axis at {}", y),
(x, 0) => println!("on x-axis at {}", x),
(x, y) => println!("at ({}, {})", x, y),
}
// Output: on y-axis at 5
Loop Labels
When you have nested loops, break and continue apply to the innermost loop by default. Rust supports loop labels to specify which loop to break or continue:
'outer: for x in 0..5 {
for y in 0..5 {
if x == 2 && y == 2 {
break 'outer; // exits both loops
}
println!("({}, {})", x, y);
}
}
Labels are prefixed with a single quote ('). This is useful for breaking out of deeply nested loops without needing flag variables.
Choosing the Right Loop
Loop Selection Guide:
+---------------------+------------------------------------------+
| Construct | When to Use |
+---------------------+------------------------------------------+
| for x in range | Known number of iterations or collection |
| while condition | Unknown iterations, condition-driven |
| loop | Infinite loop or retry-until-success |
| match | Pattern-based branching on a value |
| if / else if / else | Simple conditional branching |
+---------------------+------------------------------------------+
In idiomatic Rust, for with iterators is preferred over while with manual index management. Iterators are safer (no off-by-one errors) and more expressive.
Complete Example
fn main() {
// if as expression
let temp = 35;
let day = if temp > 20 { "sunny" } else { "cloudy" };
println!("today is {}", day);
// while with continue
let mut x = 1;
while x < 1000 {
x *= 2;
if x == 64 { continue; }
println!("x = {}", x);
}
// loop with break-value
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 5 { break counter * 10; }
};
println!("loop result = {}", result); // 50
// for with range and enumerate
for (pos, y) in (30..=32).enumerate() {
println!("{}: {}", pos, y);
}
// match with ranges and wildcard
let code = 44;
let country = match code {
44 => "UK",
46 => "Sweden",
1..=999 => "unknown",
_ => "invalid"
};
println!("code {} is {}", code, country);
}
Conclusion
Rust's flow control constructs are both familiar and uniquely powerful. The if expression eliminates the need for a ternary operator. The loop keyword makes infinite loops intentional and enables returning values from loops. The for loop with ranges and iterators prevents common off-by-one bugs. The match statement provides pattern matching that the compiler guarantees is exhaustive.
Together, these constructs give Rust code a clarity and correctness that is difficult to achieve in C or C++ without extensive discipline.