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, break can optionally return a value only when used inside a loop construct. When used inside while or for, break takes no arguments and the loop expression evaluates to ().
  • continue: Skips the rest of the current loop iteration and proceeds to the next one. For while and for, 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.