Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
716 views
in Technique[技术] by (71.8m points)

swift - Remove element from collection during iteration with forEach

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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...