在使用forEach迭代期间从集合中删除元素

Dav*_*ter 16 swift

最近,我编写了这段代码而没有多想它:

myObject.myCollection.forEach { myObject.removeItem($0) }
Run Code Online (Sandbox Code Playgroud)

myObject.removeItem(_)从哪里删除项目myObject.myCollection.

现在看代码,我很困惑为什么这个甚至有用 - 我不应该得到一个例外Collection was mutated while being enumerated吗?使用常规for-in循环时,相同的代码甚至可以正常工作!

这是预期的行为,还是我"幸运"它没有崩溃?

Ham*_*ish 20

这确实是预期的行为 - 并且是由于ArraySwift 中的一个(以及标准库中的许多其他集合)是具有写时复制语义的值类型.这意味着它的底层缓冲区(间接存储)将在变异时被复制(并且,作为优化,仅当它没有被唯一引用时).

(注意,这个答案之前曾说过,复制会在传递给迭代器时发生,正如@MartinR正确指出的那样,它不会发生什么.)

当您迭代Sequence(例如数组)时,无论是使用forEach(_:)标准for in循环还是标准循环,都会从序列的makeIterator()方法创建迭代器,并next()重复应用它的方法以便顺序生成元素.

您可以考虑迭代序列,如下所示:

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
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下Array,IndexingIterator使用a作为迭代器 - 它将通过简单地存储该集合以及迭代的当前索引来迭代给定集合的元素.每次next()调用时,基本集合都使用索引进行下标,然后递增,直到达到endIndex(您可以在此处看到它的确切实现).

因此,当你在循环中改变你的数组时,它的底层缓冲区没有被唯一引用,因为迭代器也有一个视图.这会强制缓冲区的副本 - myCollection然后使用.

所以,现在有两个数组 - 一个正在迭代的数组,另一个是你正在变异的数组.只要myCollection缓冲区保持唯一引用,循环中的任何进一步突变都不会触发另一个副本.

因此,这意味着在对枚举进行枚举时使用值语义来变异集合是完全安全的.枚举将遍历集合的整个长度 - 完全独立于您执行的任何突变,因为它们将在副本上完成.


Mar*_*n R 9

我在Apple开发者论坛上问了一个类似的问题,答案是"是的,因为Array的值语义".

@ originaluser2已经说过,但我认为会略有不同:myObject.removeItem($0)调用时,会在名称下创建并存储一个新数组myObject,但forEach()不会修改被调用的数组.

这是一个更简单的例子,展示了这种效果:

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]
Run Code Online (Sandbox Code Playgroud)