18.3Vec: iterating and borrowing
Iterating a Vec is something you'll do constantly, and it's where the borrow checker from chapter 9 earns its keep in everyday code. There are three ways to loop over a vector, matching the three ways to access any value, and there's one classic bug, modifying a collection while looping over it, that Rust turns into a compile error. This lesson covers both.
The three ways to iterate
A for loop (lesson 7.5) over a vector can borrow each element immutably, borrow it mutably, or take it by value, the same & / &mut / owned choice as any function parameter (lesson 9.4).
for x in &vec borrows each element immutably, for reading:
fn main() {
let numbers = vec![1, 2, 3];
for n in &numbers {
println!("{n}");
}
println!("still here: {numbers:?}"); // numbers still usable
}1
2
3
still here: [1, 2, 3]
&numbers borrows the vector, so the loop reads each element without consuming anything, and numbers is still usable afterward. This is the most common form.
for x in &mut vec borrows each element mutably, to change them in place:
fn main() {
let mut numbers = vec![1, 2, 3];
for n in &mut numbers {
*n *= 10;
}
println!("{numbers:?}");
}[10, 20, 30]
&mut numbers gives a mutable borrow of each element, and *n dereferences to modify it (the * from lesson 9.6). The vector must be mut. After the loop, numbers holds the changed values.
for x in vec (no &) takes each element by value, consuming the vector:
fn main() {
let words = vec![String::from("a"), String::from("b")];
for w in words {
println!("{w}"); // w owns each String
}
// println!("{words:?}"); // refused: words was moved
}a
b
Without the &, the loop moves the vector's contents out, so words is consumed and can't be used afterward (lesson 8.4). Use this when you want to take ownership of the elements (to move each into something else); use &vec when you only need to read, which is more common.
Best practice
Default to for x in &vec (borrow to read). Reach for for x in &mut vec when you need to modify elements in place, and for x in vec only when you genuinely want to consume the vector and take ownership of its elements. Accidentally writing for x in vec when you meant &vec is a common early mistake; the giveaway is a later "value moved" error on the vector. The & is rarely what you want to omit.
You can't modify a vector while iterating it
Here's the bug Rust prevents. A classic mistake in many languages is to add to or remove from a collection while looping over it, which can invalidate the loop's position and cause crashes or skipped elements. Rust makes it a compile error via the borrow checker (chapter 9):
fn main() {
let mut numbers = vec![1, 2, 3];
for n in &numbers {
if *n == 2 {
numbers.push(4); // modify while iterating: refused
}
}
}error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
--> src/main.rs:5:13
|
3 | for n in &numbers {
| --------
| |
| immutable borrow occurs here
| immutable borrow later used here
5 | numbers.push(4);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
This is E0502 from lesson 9.3, the exact same "can't have a writer while readers live" rule, here catching a real iteration bug. The for n in &numbers holds an immutable borrow of the vector for the whole loop, and numbers.push(4) needs a mutable borrow, which would have to coexist with the immutable one. The borrow checker refuses, and in doing so it prevents the genuine hazard: push might force the vector to reallocate its heap buffer (lesson 18.4), which would leave the loop's reference dangling. The rule you learned abstractly in chapter 9 is here protecting you from a concrete, notorious bug.
Key insight
"Don't mutate a collection while iterating it" is a rule other languages state in documentation and hope you follow; in Rust the borrow checker enforces it, because iterating borrows the collection and mutating needs a conflicting borrow. The same E0502 that felt abstract in chapter 9 turns out to guard one of the most common real-world collection bugs. When you hit it, the fix is usually to collect the changes you want and apply them after the loop, or to iterate over indices, or to use a method like retain that's designed for filtering in place.
Working around it
When you genuinely need to modify based on iteration, common patterns: collect the indices or values to change into a separate Vec during the loop, then apply the changes after; or use a purpose-built method like retain(|x| ...) which removes elements matching a condition safely in one call; or iterate over a range of indices (for i in 0..vec.len()) and index into the vector, since that doesn't hold a borrow across the whole loop. The borrow error is the compiler steering you toward one of these correct approaches instead of the broken one.
Quiz time
Question #1
What's the difference between for x in &vec, for x in &mut vec, and for x in vec?
Show solution
for x in &vec borrows each element immutably (read-only; the vector survives the loop). for x in &mut vec borrows each mutably (modify in place; vector must be mut, survives the loop). for x in vec takes each element by value, consuming the vector (it can't be used afterward). It's the same & / &mut / owned choice as function parameters (lesson 9.4).
Question #2
Why does the compiler refuse numbers.push(4) inside for n in &numbers?
Show solution
Because for n in &numbers holds an immutable borrow of the vector for the whole loop, and push needs a mutable borrow, which can't coexist with the immutable one (E0502, lesson 9.3). This prevents a real bug: push may reallocate the vector's buffer, which would invalidate the loop's reference. It's the chapter-9 rule catching a classic mutate-while-iterating mistake.
Question #3
You want to remove all even numbers from a Vec<i32>. Why can't you do it with a for loop that calls remove, and what's a clean alternative?
Show solution
A for loop over the vector borrows it, so calling remove (which needs a mutable borrow) inside is refused by the borrow checker, and mutating during iteration would also skip or misindex elements. The clean alternative is numbers.retain(|n| n % 2 != 0), a method built to filter in place safely in one call. (Other options: build a new filtered Vec, or iterate indices carefully.)
A vector grows as you push, but how? The next lesson looks under the hood at capacity versus length, why growth occasionally reallocates, and how with_capacity avoids repeated reallocation when you know the size ahead of time.