Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Remove element from collection during iteration with forEach

Tags:

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?

like image 765
David Ganster Avatar asked Jun 23 '16 16:06

David Ganster


People also ask

Can we remove an element by using foreach loop?

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.

Can we remove an element from a collection during iteration?

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.

How do I remove an item from a foreach?

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.

How do you remove something from a list while iterating?

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.


2 Answers

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.

like image 162
Hamish Avatar answered Sep 22 '22 10:09

Hamish


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] 
like image 28
Martin R Avatar answered Sep 19 '22 10:09

Martin R