Chapter 7: Control Flow in Rust
Control flow constructs are fundamental concepts in programming, directing the order in which code is executed based on conditions and repetition. For programmers coming from C, Rust’s control flow mechanisms will seem familiar in many ways, but there are key differences and unique features that enhance safety and expressiveness.
This chapter explores Rust’s primary control flow tools:
- Conditional execution using
if,else if, andelse. - Rust’s powerful pattern matching construct:
match. - Looping constructs:
loop,while, andfor. - The ability to use
ifandloopas expressions that produce values. - Control transfer keywords:
breakandcontinue, including labeled versions. - Key distinctions compared to control flow in C.
Rust deliberately avoids hidden control flow mechanisms like try/catch exception handling found in some other languages. Instead, potential failures are managed explicitly using the Result and Option enum types, promoting predictable code paths. These types will be covered in detail in Chapters 14 and 15.
Advanced pattern matching features, including if let and while let (which combine conditional checks with pattern matching), will be explored in Chapter 21 when we delve deeper into patterns.
7.1 Conditional Statements: if, else if, else
Conditional statements allow code execution to depend on whether a condition is true or false. Rust uses if, else if, and else, similar to C, but with important distinctions regarding type safety and usage as expressions.
7.1.1 Basic if Statements
The structure of a basic if statement is straightforward:
fn main() {
let number = 5;
// Parentheses around the condition are optional but allowed
if number > 0 {
println!("The number is positive.");
}
// Braces are always required, even for single statements
}
Key Differences from C:
-
Strict Boolean Condition: The condition must evaluate to a
booltype (trueorfalse). Rust does not implicitly convert other types (like integers) to booleans.- C Example (Implicit Conversion):
int number = 5; if (number) { // Compiles in C: non-zero integer treated as true printf("Number is non-zero.\n"); } - Rust Equivalent (Error):
You must write an explicit comparison, likefn main() { let number = 5; if number { // Compile-time error: expected `bool`, found integer println!("This won't compile"); } }if number != 0.
- C Example (Implicit Conversion):
-
Braces Required: Curly braces
{}are mandatory for the code block associated withif(andelse/else if), even if it contains only a single statement. This prevents ambiguity common in C where optional braces can lead to errors (like the “dangling else” problem or incorrect multi-statement blocks).
7.1.2 Handling Multiple Conditions: else if and else
You can chain conditions using else if and provide a default fallback using else, just like in C:
fn main() {
let number = 0;
if number > 0 {
println!("The number is positive.");
} else if number < 0 {
println!("The number is negative.");
} else {
println!("The number is zero.");
}
}
- Conditions are evaluated sequentially.
- The block associated with the first
truecondition is executed. - If no
iforelse ifcondition istrue, theelseblock (if present) is executed.
7.1.3 if as an Expression
Unlike C, where if is only a statement, Rust’s if can also be used as an expression, meaning it evaluates to a value. This is often used with let bindings and eliminates the need for a separate ternary operator (?:) like C has.
fn main() {
let condition = true;
let number = if condition {
10 // Value if condition is true
} else {
20 // Value if condition is false
}; // Semicolon for the `let` statement
println!("The number is: {}", number); // Prints: The number is: 10
}
Important Requirement: When using if as an expression, all branches (the if block and any else if or else blocks) must evaluate to values of the same type. The compiler enforces this strictly.
fn main() {
let condition = false;
let value = if condition {
5 // This is an integer (i32)
} else {
"hello" // This is a string slice (&str) - Mismatched types!
}; // Error: `if` and `else` have incompatible types
}
If an if expression is used without an else block, and the condition is false, the expression implicitly evaluates to the “unit type” (). If the if block does return a value, this leads to a type mismatch unless the if block also returns ().
fn main() {
let condition = false;
// This `if` expression implicitly returns `()` if condition is false.
let result = if condition {
println!("Condition met"); // println! returns ()
};
// 'result' will have the type ()
println!("Result is: {:?}", result); // Prints: Result is: ()
}
7.2 Pattern Matching: match
Rust’s match construct is a significantly more powerful alternative to C’s switch statement. It allows you to compare a value against a series of patterns and execute code based on the first pattern that matches.
fn main() {
let number = 2;
match number {
1 => println!("One"),
2 => println!("Two"), // This arm matches
3 => println!("Three"),
_ => println!("Something else"), // Wildcard pattern, like C's `default`
}
}
Key Features & Differences from C switch:
- Pattern-Based:
matchworks with various patterns, not just simple integer constants likeswitch. Patterns can include literal values, variable bindings, ranges (1..=5), tuple destructuring, enum variants, and more (covered in Chapter 21). - Exhaustiveness Checking: The Rust compiler requires
matchstatements to be exhaustive. This means you must cover every possible value the matched expression could have. If you don’t, your code won’t compile. The wildcard pattern_is often used as a catch-all, similar todefaultin C, to satisfy exhaustiveness. - No Fall-Through: Unlike C’s
switch, execution does not automatically fall through from onematcharm to the next. Each arm is self-contained. You do not need (and cannot use)breakstatements to prevent fall-through between arms. matchas an Expression: Likeif,matchis also an expression. Each arm must evaluate to a value of the same type if thematchexpression is used to produce a result.
fn main() {
let number = 3;
let result_str = match number {
0 => "Zero",
1 | 2 => "One or Two", // Multiple values with `|`
3..=5 => "Three to Five", // Inclusive range
_ => "Greater than Five",
};
println!("Result: {}", result_str); // Prints: Result: Three to Five
}
match is one of Rust’s most powerful features for control flow and data extraction, especially when working with enums like Option and Result.
7.3 Loops
Rust provides three looping constructs: loop, while, and for. Each serves different purposes, and they incorporate Rust’s emphasis on safety and expression-based evaluation. Notably, Rust does not have a direct equivalent to C’s do-while loop.
7.3.1 The Infinite loop
The loop keyword creates a loop that repeats indefinitely until explicitly stopped using break.
fn main() {
let mut counter = 0;
loop {
println!("Again!");
counter += 1;
if counter == 3 {
break; // Exit the loop
}
}
}
loop as an Expression: A unique feature of loop is that break can return a value from the loop, making loop itself an expression. This is useful for retrying operations until they succeed.
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
// Pass the value back from the loop using break
break counter * 2;
}
};
println!("The result is: {}", result); // Prints: The result is: 20
}
7.3.2 Conditional Loops: while
The while loop executes its body as long as a condition remains true. It checks the condition before each iteration.
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
}
As with if, the condition for while must evaluate to a bool. There’s no implicit conversion from integers.
Emulating do-while: C’s do-while loop executes the body at least once before checking the condition. You can achieve this in Rust using loop with a conditional break at the end:
fn main() {
let mut i = 0;
// Equivalent to C: do { ... } while (i < 5);
loop {
println!("Current i: {}", i);
i += 1;
if !(i < 5) { // Check condition at the end
break;
}
}
}
7.3.3 Iterator Loops: for
Rust’s for loop is fundamentally different from C’s traditional three-part for loop (for (init; condition; increment)). Instead, Rust’s for loop iterates over elements produced by an iterator. This is a safer and often more idiomatic way to handle sequences.
Iterating over a Range:
fn main() {
// `0..5` is a range producing 0, 1, 2, 3, 4 (exclusive end)
for i in 0..5 {
println!("The number is: {}", i);
}
// `0..=5` is a range producing 0, 1, 2, 3, 4, 5 (inclusive end)
for i in 0..=5 {
println!("Inclusive range: {}", i);
}
}
Iterating over Collections (like Arrays):
fn main() {
let a = [10, 20, 30, 40, 50];
// `a.iter()` creates an iterator over the elements of the array
for element in a.iter() {
println!("The value is: {}", element);
}
// Or more concisely, `for element in a` also works for arrays
for element in a {
println!("Again: {}", element);
}
}
Rust’s for loop, by working with iterators, prevents common errors like off-by-one mistakes often associated with C-style index-based loops. We will discuss iterators in more detail later.
7.3.4 Controlling Loop Execution: break and continue
Rust supports break and continue within all loop types (loop, while, for), behaving similarly to their C counterparts:
break: Immediately exits the innermost loop it’s contained within.- As noted earlier,
breakcan optionally return a value only when used inside aloopconstruct. When used insidewhileorfor,breaktakes no arguments and the loop expression evaluates to().
- As noted earlier,
continue: Skips the rest of the current loop iteration and proceeds to the next one. Forwhileandfor, this involves re-evaluating the condition or getting the next iterator element, respectively.
7.3.5 Labeled Loops for Nested Control
Sometimes you need to break or continue an outer loop from within an inner loop. C often requires goto or boolean flags for this. Rust provides a cleaner mechanism using loop labels.
A label is defined using a single quote followed by an identifier (e.g., 'outer:) placed before the loop statement. break or continue can then specify the label to target.
fn main() {
let mut count = 0;
'outer: loop { // Label the outer loop
println!("Entered the outer loop");
let mut remaining = 10;
loop { // Inner loop (unlabeled)
println!("remaining = {}", remaining);
if remaining == 9 {
// Breaks only the inner loop
break;
}
if count == 2 {
// Breaks the outer loop using the label
break 'outer;
}
remaining -= 1;
}
count += 1;
}
println!("Exited outer loop. Count = {}", count); // Prints: Count = 2
}
fn main() {
'outer: for i in 0..3 {
for j in 0..3 {
if i == 1 && j == 1 {
// Skip the rest of the 'outer loop's current iteration (i=1)
// and proceed to the next iteration (i=2)
continue 'outer;
}
println!("i = {}, j = {}", i, j);
}
}
}
// Output skips pairs where i is 1 after j reaches 1:
// i = 0, j = 0
// i = 0, j = 1
// i = 0, j = 2
// i = 1, j = 0
// i = 2, j = 0
// i = 2, j = 1
// i = 2, j = 2
Labeled break and continue offer precise control over nested loop execution without resorting to less structured approaches like goto.
7.4 Summary
This chapter covered Rust’s core control flow mechanisms, highlighting similarities and key differences compared to C:
-
Conditional Statements (
if/else if/else):- Conditions must be
bool; no implicit integer-to-boolean conversion. - Braces
{}are mandatory for all blocks. ifcan be used as an expression, requiring type consistency across branches. This often replaces C’s ternary operator (?:).
- Conditions must be
-
Pattern Matching (
match):- A powerful construct replacing C’s
switch. - Matches against complex patterns, not just constants.
- Enforces exhaustiveness (all possibilities must be handled).
- No fall-through behaviour;
breakis not needed between arms. - Can be used as an expression.
- A powerful construct replacing C’s
-
Looping Constructs:
loop: An infinite loop, breakable withbreak, which can return a value.while: Condition-based loop checking the boolean condition before each iteration.for: Iterator-based loop for ranges and collections, promoting safety over C-style index loops.- No direct
do-whileequivalent, but easily emulated withloopandbreak.
-
Loop Control:
breakexits the current loop (optionally returning a value fromloop).continueskips to the next iteration.- Loop labels (
'label:) allowbreakandcontinueto target specific outer loops in nested structures, providing clearer control than C’sgotoor flag variables.
Rust’s control flow design emphasizes explicitness, type safety, and expressiveness. Features like match, expression-based if/loop, and labeled breaks help prevent common bugs found in C code and allow for more robust and readable programs. Mastering these constructs is essential for writing effective Rust code. The following chapters will build upon these foundations, particularly when exploring error handling and more advanced pattern matching.