Polyglot
Languages Rust loops
Rust § loops

Loops

Rust provides three loop forms: for (the conventional iteration over an iterable), while (condition-driven), and loop (infinite, exited with break). The for loop is built on the Iterator trait; any type implementing IntoIterator admits being iterated. Iterators are lazy — adapters like map and filter produce new iterators without materialising; the work happens at the consumer (e.g., collect, sum, for_each). The combination — explicit iterator-based loops, lazy adapters, value-producing loop with break value — covers the iteration surface.

This page covers the loop forms; the iterator surface is in Iterators.

for

The for loop iterates over an iterable:

for n in 0..10 {
    println!("{}", n);                          // 0, 1, ..., 9
}

let v = vec![1, 2, 3, 4];
for x in &v {                                   // borrow; v retains ownership
    println!("{}", x);
}

for x in v.iter() {                              // explicit
    println!("{}", x);
}

The for x in expr calls expr.into_iter() (if not already an iterator) and iterates. The standard library’s collection types implement IntoIterator:

let v = vec![1, 2, 3];

for x in v {                                    // moves v; consumed
    println!("{}", x);
}
// v is no longer accessible

let v = vec![1, 2, 3];
for x in &v {                                   // borrows v
    println!("{}", x);
}
// v still accessible

for x in &mut v {                               // mutable borrow
    *x *= 2;
}

The three forms differ in ownership:

FormEffect
for x in vMoves v; iterating consumes it
for x in &vBorrows v immutably
for x in &mut vBorrows v mutably

The conventional discipline is to borrow (for x in &v) when possible; move (for x in v) only when consuming.

enumerate for index and value

let v = vec!["a", "b", "c"];
for (i, x) in v.iter().enumerate() {
    println!("{}: {}", i, x);
}

The enumerate adapter pairs each item with its index.

zip for parallel iteration

let a = vec![1, 2, 3];
let b = vec!["a", "b", "c"];

for (x, y) in a.iter().zip(b.iter()) {
    println!("{} → {}", x, y);
}

The zip admits iterating two iterables together; stops at the shorter.

while

The while loop tests a condition before each iteration:

let mut n = 0;
while n < 10 {
    println!("{}", n);
    n += 1;
}

The condition must be a bool. The conventional uses are polling, condition-driven termination, and loops where the iteration count isn’t known.

For range-based iteration with a step, for over a range is conventionally cleaner than while:

// while:
let mut n = 0;
while n < 100 {
    println!("{}", n);
    n += 5;
}

// for (preferable):
for n in (0..100).step_by(5) {
    println!("{}", n);
}

while let

Conditional iteration with pattern matching:

let mut stack = vec![1, 2, 3];

while let Some(top) = stack.pop() {
    println!("{}", top);
}

The loop continues as long as the pattern matches; on mismatch, the loop exits.

The conventional uses are draining queues, processing iterators with potential failures, and retry loops:

let mut queue = VecDeque::new();
// ... push items ...

while let Some(item) = queue.pop_front() {
    process(item);
}

loop

The loop keyword introduces an infinite loop:

loop {
    let input = read_input();
    if input == "quit" {
        break;
    }
    process(input);
}

loop is conventionally used when the termination condition depends on internal state rather than an upfront predicate.

loop admits returning a value via break value:

let result = loop {
    let attempt = try_compute();
    if let Ok(value) = attempt {
        break value;
    }
};

The break value form admits using loop as a retry-with-result expression.

break and continue

break exits the innermost enclosing for, while, or loop:

for n in 0..100 {
    if n * n > 1000 {
        break;
    }
    println!("{}", n);
}

continue skips the rest of the current iteration and proceeds to the next:

for n in 0..10 {
    if n % 2 == 0 {
        continue;
    }
    println!("{}", n);                          // odd numbers
}

For nested loops, labels admit targeting a specific loop:

'outer: for i in 0..10 {
    for j in 0..10 {
        if i * j > 50 {
            break 'outer;
        }
    }
}

The 'outer: introduces a label; break 'outer and continue 'outer target it.

Iterator-based loops

Rust’s for loops desugar to iterator-based code:

for x in iter {
    body
}

// Equivalent to:
let mut iter = iter.into_iter();
loop {
    match iter.next() {
        Some(x) => { body; }
        None => break,
    }
}

The mechanism admits using iterator adapters and consumers in many places where explicit loops would be needed:

// Explicit:
let mut total = 0;
for x in &v {
    if *x > 0 {
        total += x;
    }
}

// Iterator-based:
let total: i32 = v.iter()
    .filter(|&&x| x > 0)
    .sum();

The iterator-based form is conventionally clearer for filter-and-aggregate patterns. Treated in Iterators.

Common patterns

Iterating with index

for (i, x) in v.iter().enumerate() {
    println!("[{}] = {}", i, x);
}

Iterating over a range

for n in 0..10 {                                // 0, 1, ..., 9
    // ...
}

for n in 0..=10 {                                // 0, 1, ..., 10 (inclusive)
    // ...
}

for n in (0..100).step_by(5) {                  // 0, 5, ..., 95
    // ...
}

for n in (0..10).rev() {                        // 9, 8, ..., 0
    // ...
}

Iterating over a map

use std::collections::HashMap;

let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);

for (key, value) in &map {
    println!("{}: {}", key, value);
}

for key in map.keys() {
    println!("{}", key);
}

for value in map.values() {
    println!("{}", value);
}

The map admits iterating over entries (keys and values together), keys, or values.

Iterating with a step

let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (i, x) in v.iter().enumerate() {
    if i % 2 == 0 {
        println!("{}", x);
    }
}

// Or with iterator adapters:
for x in v.iter().step_by(2) {
    println!("{}", x);
}

The step_by adapter admits “every nth element”.

Consuming an iterator

let v = vec![1, 2, 3, 4, 5];

let total: i32 = v.iter().sum();
let max = v.iter().max().unwrap();
let count = v.iter().filter(|&&x| x > 2).count();
let doubled: Vec<i32> = v.iter().map(|&x| x * 2).collect();

The conventional Rust style favours iterator chains over explicit loops for these patterns.

Loop with a counter

let mut count = 0;
loop {
    if count >= 10 {
        break;
    }
    println!("{}", count);
    count += 1;
}

// Or simply:
for n in 0..10 {
    println!("{}", n);
}

For known iteration counts, for over a range is conventionally clearer.

Reading lines from stdin

use std::io::{self, BufRead};

let stdin = io::stdin();
for line in stdin.lock().lines() {
    let line = line?;
    process(&line);
}

The lines() returns an iterator over Result<String, io::Error>; treated in I/O.

Modifying during iteration

Rust’s borrow checker prevents modifying a collection during iteration (would invalidate the iterator):

let mut v = vec![1, 2, 3];
for x in &v {
    v.push(4);                                  // ERROR: v is borrowed
}

The conventional defences:

  • Iterate a range and index:
for i in 0..v.len() {
    if v[i] > 0 {
        v.push(v[i] * 2);                       // OK; index-based
    }
}
  • Use retain for removal:
v.retain(|&x| x > 0);
  • Build a new vector:
let new_v: Vec<i32> = v.iter().filter(|&&x| x > 0).collect();
  • Use drain for consuming with state:
let mut v = vec![1, 2, 3, 4];
let drained: Vec<i32> = v.drain(..).filter(|&x| x > 0).collect();

Infinite stream

loop {
    let item = source.next();
    match item {
        Some(x) => process(x),
        None => continue,                        // wait for more
    }
}

For network and message-driven loops; the conventional Rust pattern.

Early termination from a loop

let result = loop {
    let response = request();
    match response {
        Ok(value) => break Ok(value),
        Err(e) if is_retryable(&e) => {
            std::thread::sleep(std::time::Duration::from_secs(1));
            continue;
        }
        Err(e) => break Err(e),
    }
};

The pattern admits retry logic with explicit termination.

Loop expressions

loop, while, for, and while let are expressions — they produce values:

  • loop — its value is whatever break value produces; without break value, the type is ! (never).
  • while, for, while let — their value is () (unit); they cannot produce non-unit values.
let result = loop {
    if condition() {
        break 42;
    }
};
// result is i32

let unit = while condition() {                  // unit; can't capture a value
    // ...
};

The asymmetry (only loop admits break value) reflects that for and while may not execute their bodies at all (the condition may be initially false), and the value would be undefined.

A note on the absence of do-while

Rust does not have a do-while. The conventional substitute:

loop {
    do_work();
    if !condition() {
        break;
    }
}

Or:

let mut first = true;
while first || condition() {
    do_work();
    first = false;
}

The loop-with-break form is the conventional Rust idiom.

A note on the conventional discipline

The contemporary Rust loop advice:

  • Use for for iteration over collections and ranges.
  • Use while for condition-driven loops.
  • Use loop for infinite loops and value-producing retries.
  • Use iterator adapters (map, filter, sum, collect) when the loop body is a transformation.
  • Use enumerate for index-and-value iteration.
  • Use zip for parallel iteration.
  • Use labels for nested-loop control — rarely, but useful.

The combination — for for iteration, while for condition, loop for value-producing — is the substance of Rust’s loop surface. The conventional discipline is to choose the form that admits the simplest expression of the algorithm.