Control Flow · about 40 min

for Loops, Ranges and Iterating Arrays

Why this matters

Most loops in real programs walk over something: the numbers 1 to 100, every element of an array, every line of input. for expresses exactly that, without the manual counter bookkeeping that causes off-by-one bugs and out-of-bounds reads. It is by far the most common loop in Rust, and it is your first contact with iterators, the mechanism behind a large part of idiomatic Rust (module 11).

for over a range

fn main() {
    for i in 1..4 {
        println!("i = {i}");
    }
    for i in 1..=4 {
        print!("{i} ");
    }
    println!();
}
i = 1
i = 2
i = 3
1 2 3 4 

Half-open ranges are the convention because they combine well with zero-based indexes: an array of length n has valid indexes 0..n, a range of length b - a is a..b, and splitting 0..n at m gives 0..m and m..n with nothing lost or doubled.

A range is a value like any other. It can be stored in a variable, and it has methods:

fn main() {
    let r = 0..10;
    println!("{:?} has {} elements, contains 5: {}", r, r.len(), r.contains(&5));
    for i in (0..10).step_by(3) {
        print!("{i} ");
    }
    println!();
    for i in (1..=5).rev() {
        print!("{i} ");
    }
    println!();
}
0..10 has 10 elements, contains 5: true
0 3 6 9 
5 4 3 2 1 

step_by(3) takes every third value; rev() goes backwards. The parentheses around the range matter: 1..=5.rev() would try to call rev on the number 5.

Iterating over an array

You could loop over indexes:

fn main() {
    let temps = [12, 15, 9, 20, 17];
    for i in 0..temps.len() {
        println!("day {i}: {}", temps[i]);
    }
}

That works, but idiomatic Rust loops over the elements directly:

fn main() {
    let temps = [12, 15, 9, 20, 17];
    let mut total = 0;
    for t in temps {
        total += t;
    }
    println!("total {total}, average {:.1}", total as f64 / temps.len() as f64);

    for (day, t) in temps.iter().enumerate() {
        if *t > 16 {
            println!("day {day} was warm: {t}");
        }
    }
}
total 73, average 14.6
day 3 was warm: 20
day 4 was warm: 17

You will often see the pattern for (i, &v) in values.iter().enumerate(): the &v in the pattern takes the reference apart so that v is a plain i32.

Use the index form only when you really need indexes for something else, such as comparing an element with its neighbor values[i - 1].

What for really does

for x in thing works with anything that can produce an iterator: a value with a method next() that returns the next item, or a signal that there are no more. The loop is shorthand for:

let mut it = thing.into_iter();
loop {
    match it.next() {
        Some(x) => { body using x }
        None => break,
    }
}

A range is its own iterator: next() returns the current number and advances. An array's iterator walks its elements. io::stdin().lines() from the last module is an iterator that reads a line each time next() is called. That is why the same for syntax works for all of them, and why you will be able to write your own iterators in module 11. Some, None and match are coming up very soon (the next lesson and module 8).

Accumulator patterns

Many loops follow one of a few shapes. Recognizing them makes loops easy to write:

fn main() {
    let values = [4, -2, 9, 0, 9, 3];

    let mut sum = 0;                       // accumulate
    let mut count_positive = 0;            // count matches
    let mut max = values[0];               // track best so far
    let mut first_zero = None;             // find first match
    for (i, &v) in values.iter().enumerate() {
        sum += v;
        if v > 0 {
            count_positive += 1;
        }
        if v > max {
            max = v;
        }
        if v == 0 && first_zero.is_none() {
            first_zero = Some(i);
        }
    }
    println!("sum {sum}, positive {count_positive}, max {max}, first zero {:?}", first_zero);
}
sum 23, positive 4, max 9, first zero Some(3)

Note how max starts at the first element rather than 0: with all-negative data, 0 would win incorrectly. first_zero uses Option (None or Some(index)), which you will meet properly in module 8.

Common mistakes

Off-by-one with ranges. 1..10 stops at 9. If you want 10, write 1..=10.

Modifying the array while iterating it. for t in temps.iter() { temps[0] = 1; } is rejected: you cannot change the array while something is reading through it. The error (E0506 or E0502) belongs to the borrowing rules of module 5, and it prevents a real class of bugs.

Using an index variable as the wrong type. Indexes are usize. for i in 0..n where n is an i32 gives an i32 that cannot index an array: "the type [i32] cannot be indexed by i32".

Expecting a reversed range to work without rev:

fn main() {
    for i in 5..1 {
        println!("{i}");
    }
    println!("done");
}
done

5..1 is simply empty. Count down with (1..5).rev(), which gives 4, 3, 2, 1.

Recap

Going further: print the characters of a word with for c in "hello".chars() and their positions with .enumerate(). Try for (a, b) in [1, 2, 3].iter().zip([10, 20, 30].iter()) and guess what it does before running it.

Exercises

Exercise 1 · quiz

Counting a range

How many times does the body of for i in 3..8 { ... } run, and what is the last value of i?

Answer
  • 5 times, last 7

a..b is half-open: it includes a and excludes b, so 3, 4, 5, 6, 7 is five values and the count is simply b - a. Write 3..=8 for the inclusive range, which runs six times and ends at

  1. Half-open ranges fit zero-based indexing perfectly: 0..len visits every valid index.

Exercise 2 · write code

Multiplication table

Read n from stdin and print the multiplication table from 1 to n, each number right-aligned in a width of 4. For input 3:

   1   2   3
   2   4   6
   3   6   9

Use two nested for loops over inclusive ranges, print! for each number and println!() at the end of each row.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
}

Example

input

3

expected output

   1   2   3
   2   4   6
   3   6   9

Example

input

1

expected output

   1
Hint 1

for row in 1..=n { for col in 1..=n { ... } println!(); }

Hint 2

print!("{:4}", row * col) prints one number right-aligned in 4 columns without a newline.

Reference solution
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
    for row in 1..=n {
        for col in 1..=n {
            print!("{:4}", row * col);
        }
        println!();
    }
}

Exercise 3 · write code

Largest value and where it is

Write fn largest(values: [i32; 6]) -> (usize, i32) that returns the index and value of the largest element (the first one if the maximum appears more than once). Loop with for (i, &v) in values.iter().enumerate() or over the indexes 0..values.len().

The given main prints:

largest is 42 at index 3

Hidden tests check other arrays, including all-negative ones.

File: main.rs

Starting point

fn largest(values: [i32; 6]) -> (usize, i32) {
    (0, 0)
}

fn main() {
    let data = [7, -3, 19, 42, 0, 42];
    let (i, v) = largest(data);
    println!("largest is {v} at index {i}");
}

Example

expected output

largest is 42 at index 3
Hint 1

Start with the first element as the best so far, not with 0: with all-negative input, 0 would wrongly win.

Hint 2

Use > (not >=) so the first maximum is kept.

Reference solution
fn largest(values: [i32; 6]) -> (usize, i32) {
    let mut best_i = 0;
    let mut best = values[0];
    for (i, &v) in values.iter().enumerate() {
        if v > best {
            best = v;
            best_i = i;
        }
    }
    (best_i, best)
}

fn main() {
    let data = [7, -3, 19, 42, 0, 42];
    let (i, v) = largest(data);
    println!("largest is {v} at index {i}");
}

Exercise 4 · write code

Stepping and reversing

Read n from stdin. Print, on one line separated by single spaces, the even numbers from 0 up to and including n (if n is even), then on a second line the numbers from n down to 1. For input 7:

0 2 4 6
7 6 5 4 3 2 1

Use step_by and rev. Hint for the separators: print a space before every number except the first. (Trailing spaces are ignored by the checker, but try to avoid them.)

File: main.rs

Starting point

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
}

Example

input

7

expected output

0 2 4 6
7 6 5 4 3 2 1

Example

input

4

expected output

0 2 4
4 3 2 1
Hint 1

(0..=n).step_by(2) gives 0, 2, 4, ...; (1..=n).rev() counts down. The parentheses are needed.

Hint 2

In the first loop the first number is 0, so if i > 0 { print!(" "); } before printing works.

Reference solution
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
    for i in (0..=n).step_by(2) {
        if i > 0 {
            print!(" ");
        }
        print!("{i}");
    }
    println!();
    for i in (1..=n).rev() {
        if i < n {
            print!(" ");
        }
        print!("{i}");
    }
    println!();
}