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:
| Form | Effect |
|---|---|
for x in v | Moves v; iterating consumes it |
for x in &v | Borrows v immutably |
for x in &mut v | Borrows 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
retainfor removal:
v.retain(|&x| x > 0);
- Build a new vector:
let new_v: Vec<i32> = v.iter().filter(|&&x| x > 0).collect();
- Use
drainfor 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 whateverbreak valueproduces; withoutbreak 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
forfor iteration over collections and ranges. - Use
whilefor condition-driven loops. - Use
loopfor infinite loops and value-producing retries. - Use iterator adapters (
map,filter,sum,collect) when the loop body is a transformation. - Use
enumeratefor index-and-value iteration. - Use
zipfor 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.