Loops

If you read the Control flow, you have already been introduced to loops. To recap, there are basically three ways to create a loop:

  • for
  • loop
  • while

for loops

A super common way to repeat an action a number of times is with a for loop:

fn main() {
    for i in 0..10 {
        print!("{} ", i)
    }

    println!();
}

Like many other development languages, Rust also supports custom step-sizes while iterating:

fn main() {
    for i in (0..10).step_by(2) {
        print!("{} ", i)
    }

    println!();
}

... or reverse traversal:

fn main() {
    for i in (0..10).rev() {
        print!("{} ", i)
    }

    println!();
}

loop loops

For custom scenario's a loop might be more suited. loop loops are often combined with a break statement to exit the loop. As per the Rust book, it can be very useful to capture the result of a loop in a variable.

fn main() {
    let mut i = 103;

    let result = loop {
        i += 1;
        if i % 25 == 0 {
            break i;
        }
    };

    println!("{result} can be divided by 25");
}

Of course, you could just print the value of i in the above example. Pretend you didn't notice that.

inner loops

Rust supports labels for loops. This can be useful when you have nested loops, and you want to break out of the outer loop from the inner loop.

fn main() {
    'outer: loop {
        println!("Entered the outer loop");
        loop {
            println!("Entered the inner loop");
            break 'outer;
        }

        println!("This point will never be reached");
    }

    println!("Exited the outer loop");
}

while loops

Conditional loops with while might be used to achieve a similar result.

fn main() {
    let mut i = 103;

    while i % 25 != 0 {
        i += 1;
    }

    println!("{i} can be divided by 25");
}

Don't go crazy!

As you've seen, results of if, match and loop blocks can be assigned to a variable. Although this is a very powerful mechanism, don't go crazy on nesting these variations. The fact that something can be done, does not mean it must be done. Someone needs to maintain and debug this code!

Imagine you need to work on something like the below:

fn main() {
    for i in &[0, 2, 4, 17, 29, 45, 102] {
        let age = *i;
        let description = match age {
            0..=9 => {
                if age < 2 {
                    "baby"
                } else {
                    match age {
                        2..=3 => "toddler",
                        _ => "child",
                    }
                }
            }
            10..=17 => "teenager",
            a => {
                if a < 30 {
                    "young adult"
                } else {
                    let mut i = a;
                    loop {
                        i += 1;
                        if i == 65 {
                            break "adult";
                        } else if i > 65 {
                            break "elderly";
                        }
                    }
                }
            }
        };

        println!("{age} = {description}");
    }
}

Using variables in loops

Let's have a quick look at the below example. At first glance, this may seem a valid code, greeting someone 10 times.

fn greet(name: String) {
    println!("Welcome {name}");
}

fn main() {
    let name = "Marcel".to_string();
    for _i in 0..10 {
        greet(name);
    }
}

When we compile the code, we are greeted with our old friend: error[E0382]: use of moved value: 'name'. This happens, because the name variable and the ownership of the name variable is passed to the greet() function during the first execution of the loop. Subsequent iterations therefore no longer have access to the name variable and can't run.

In this case we can easily borrow the name variable to get us out of this situation:

fn greet(name: &str) {
    println!("Welcome {name}");
}

fn main() {
    let name = "Marcel".to_string();
    for _i in 0..10 {
        greet(&name);
    }
}

Reference material