What goes wrong when a loop changes what it walks over
A loop that repeats forever is called an infinite loop. One classic way to build one is to add items to a collection while the loop is still walking over it. Each pass makes the collection longer, so the loop never reaches the end.
Here is that situation written with a while loop and an index:
let mut fruits: Vec<&str> = vec!["banana", "orange"];
let mut i = 0;
while i < fruits.len() {
fruits.push("kivi");
i += 1;
}
The condition i < fruits.len() is checked again before every pass, and fruits.len() is read fresh each time. After the first pass i is 1 and the length is 3. After the second pass i is 2 and the length is 4. The length is always one step ahead of i, so the condition stays true and the loop only stops when the program runs out of memory or is killed.
The fix for this version is to decide how many passes you want before the loop starts, for example by saving let n = fruits.len(); and looping while i < n. Then pushing inside the loop cannot move the finish line.
Why Rust stops you in a for loop
In most languages you can compile an infinite loop like the one above, and also its for cousin that appends to a list while iterating over it. Rust is stricter. A for loop over a vector holds the vector for the whole loop, and the borrow checker does not allow another piece of code to change it at the same time. So the infinite version of the for loop usually does not compile at all.
There are two ways to hold the vector, and they fail in different ways.
- Looping over the vector itself, as in
for x in v. The loop callsinto_iter(), which takes ownership: the vector is moved into the loop. After that, the namevcan no longer be used, not even inside the loop body. The compiler reportsborrow of moved value. - Looping over a reference, as in
for x in &v. The vector stays alive, but it is borrowed as read-only for the whole loop. Callingv.push(...)inside needs a mutable borrow, and the compiler reportscannot borrowthe vectoras mutable because it is also borrowed as immutable.
Either way the mistake is caught before the program runs, which is better than finding it by a frozen terminal.
The exercise: fix the broken loop
The lesson's program wants to add every name from new_students to the end of students. The expected output is Andy, Mary, Alex, Ana, Alexa and Mike. The starting code does not compile:
let mut students = vec!["Andy", "Mary", "Alex"];
let new_students = vec!["Ana", "Alexa", "Mike"];
for student in new_students {
new_students.push(student);
}
The compiler reports two errors for it. The first is borrow of moved value: new_students, with the note that the vector was moved by the implicit into_iter() call of the for loop. The second is that new_students cannot be borrowed as mutable, because it was declared without mut.
Both errors come from one wrong idea: the loop pushes into the same vector it is reading. That is also the shape of the infinite loop. Think about what each name is for:
new_studentsis the source. The loop reads from it, so nothing should be pushed into it.studentsis the destination. It is the vector declared withmut, because it is the one that changes.
So the push belongs on students. Reading from one vector and writing into another can never loop forever, because the vector being read does not grow.
let mut students = vec!["Andy", "Mary", "Alex"];
let new_students = vec!["Ana", "Alexa", "Mike"];
for student in new_students {
students.push(student);
}
Printing students afterwards gives all six names in order: Andy, Mary, Alex, Ana, Alexa, Mike. The loop moves new_students into itself, and each student is moved out of it and into students. This is fine because the program does not need new_students afterwards.
Other ways to write it
If you still need new_students after the loop, loop over a reference and push a copy of each item. The items here are &str values, which are cheap to copy:
for student in &new_students {
students.push(*student);
}
Here student is a &&str, so *student gives the &str that students holds. Reading new_students while writing students is allowed, because they are two different vectors. Printing new_students after this loop still works.
For this exact job there is also a method that does the whole loop: students.extend(new_students); moves every item over and leaves the same six names in students.
Key takeaways
- Growing a collection while looping over it makes the loop's end move away from you. In a
whileloop withfruits.len()in the condition, it never ends. - In Rust, a
forloop over a vector owns or borrows it for the whole loop, so changing that same vector inside the loop is a compile error, not a hang. for x in vmovesvinto the loop.for x in &vborrows it as read-only.- Read from one collection and push into another, or save the length in a variable before a
whileloop begins. - Only the collection that changes needs
mut.
