Remove element from collection during iteration with forEach

This is indeed expected behaviour – and is due to the fact that an Array in Swift (as well as many other collections in the standard library) is a value type with copy-on-write semantics. This means that its underlying buffer (which is stored indirectly) will be copied upon being mutated (and, as an optimisation, only when it's not uniquely referenced).

When you come to iterate over a Sequence (such as an array), be it with forEach(_:) or a standard for in loop, an iterator is created from the sequence's makeIterator() method, and its next() method is repeatedly applied in order to sequentially generate elements.

You can think of iterating over a sequence as looking like this:

let sequence = [1, 2, 3, 4]
var iterator = sequence.makeIterator()

// `next()` will return the next element, or `nil` if
//  it has reached the end sequence.
while let element = iterator.next() { 
    // do something with the element
}

In the case of Array, an IndexingIterator is used as its iterator – which will iterate through the elements of a given collection by simply storing that collection along with the current index of the iteration. Each time next() is called, the base collection is subscripted with the index, which is then incremented, until it reaches endIndex (you can see its exact implementation here).

Therefore, when you come to mutate your array in the loop, its underlying buffer is not uniquely referenced, as the iterator also has a view onto it. This forces a copy of the buffer – which myCollection then uses.

So, there are now two arrays – the one which is being iterated over, and the one you're mutating. Any further mutations in the loop won't trigger another copy, as long as myCollection's buffer remains uniquely referenced.

This therefore means that it's perfectly safe to mutate a collection with value semantics while enumerating over it. The enumeration will iterate over the full length of the collection – completely independant of any mutations you do, as they will be done on a copy.


I asked a similar question in the Apple Developer Forum and the answer is "yes, because of the value semantics of Array".

@originaluser2 said that already, but I would argue slightly different: When myObject.removeItem($0) is called, a new array is created and stored under the name myObject, but the array that forEach() was called upon is not modified.

Here is a simpler example demonstrating the effect:

extension Array {
    func printMe() {
        print(self)
    }
}

var a = [1, 2, 3]
let pm = a.printMe // The instance method as a closure.
a.removeAll() // Modify the variable `a`.
pm() // Calls the method on the value that it was created with.
// Output: [1, 2, 3]

Tags:

Swift