The problem: changing a sequence while looping over it
Loops walk over sequences such as arrays and strings. Trouble starts when the loop body changes the very sequence it is walking over. In some languages that makes a loop run forever. Swift behaves differently for for-in, but the same idea can still produce an infinite loop elsewhere.
A for-in loop works on a copy
Consider this code:
var fruits = ["banana", "orange"]
for _ in fruits {
fruits.append("kivi")
}
Each pass appends an item to the array being iterated. Swift arrays are values, so the loop walks through a copy of fruits made when the loop starts. That copy holds two items, so the body runs twice, and afterwards fruits has four items. The loop is not infinite. The same holds for for _ in 0..<fruits.count: the range is computed once, before the first pass, and also gives two passes.
A while loop re-reads the size
The trap appears when the condition is checked on every pass against the array's current size:
var i = 0
while i < fruits.count {
fruits.append("kivi")
i += 1
}
Every pass adds one item and increases i by one, so i never catches up with fruits.count: after a pass, count is always two larger than i. The condition stays true and the loop never ends. This is an infinite loop. It keeps the program running and growing memory until the program is stopped or crashes, so the code is unusable.
A bounded version shows the pattern. Stopping after five passes, starting from the two-item array, leaves i at 5 and the array at 7 items, and the condition is still true.
The exercise
The starting code should add every name from newStudents to the end of students:
var students = ["Andy", "Mary", "Alex"]
let newStudents = ["Ana", "Alexa", "Mike"]
for student in newStudents {
newStudents.append(student)
}
It does not compile. The compiler says cannot use mutating member on immutable value: 'newStudents' is a 'let' constant. Two things are wrong at once: the loop walks over newStudents, which is right, but it appends to newStudents as well. That is the pattern this lesson warns about, and because newStudents is a let, Swift refuses it.
- Read the loop header. It walks over the source array,
newStudents. Keep it. - Find the array that must grow. The target is
students, declared withvar, so it can change. - Append to the target. Change the body to
students.append(student).
var students = ["Andy", "Mary", "Alex"]
let newStudents = ["Ana", "Alexa", "Mike"]
for student in newStudents {
students.append(student)
}
Now the loop reads from one array and writes to another. It runs three times and students ends as ["Andy", "Mary", "Alex", "Ana", "Alexa", "Mike"].
Common mistakes
- Changing
lettovaronnewStudents. The code would compile, but it would append to the array it is iterating and never fillstudents. - Appending to the array you loop over. With
for-init finishes, but the result is rarely what you meant. Loop over one array and build another. - Forgetting to advance a while loop. Without
i += 1the condition never changes. Swift has no++, so write+= 1.
Key takeaways
- A
for-inloop over an array uses a copy made when the loop starts, so appending inside it does not make it endless. - A
whilecondition that reads the current size can chase a growing array forever. - A
letarray cannot be changed. Read from one array and append to a differentvararray.
