Recently, I wrote this code without thinking about it very much:
myObject.myCollection.forEach { myObject.removeItem($0) }
where myObject.removeItem(_)
removes an item from myObject.myCollection
.
Looking at the code now, I am puzzled as to why this even works - shouldn't I get an exception along the lines of Collection was mutated while being enumerated
? The same code even works when using a regular for-in loop!
Is this expected behaviour or am I 'lucky' that it isn't crashing?
The program needs access to the iterator in order to remove the current element. The for-each loop hides the iterator, so you cannot call remove . Therefore, the for-each loop is not usable for filtering.
An element can be removed from a Collection using the Iterator method remove(). This method removes the current element in the Collection. If the remove() method is not preceded by the next() method, then the exception IllegalStateException is thrown.
Use unset() function to remove array elements in a foreach loop. The unset() function is an inbuilt function in PHP which is used to unset a specified variable.
If you want to delete elements from a list while iterating, use a while-loop so you can alter the current index and end index after each deletion.
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]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With