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

时间:2016-06-23 16:48:45

标签: swift

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

myObject.myCollection.forEach { myObject.removeItem($0) }

myObject.removeItem(_)myObject.myCollection删除项目。

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

这是预期的行为还是“我很幸运”它没有崩溃?

3 个答案:

答案 0 :(得分:16)

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

当您来遍历Sequence(例如数组)时,无论是forEach(_:)还是标准for in循环,都会从序列{{3}创建迭代器方法,并重复应用其makeIterator()方法,以便顺序生成元素。

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

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
}

Array的情况下,next()被用作其迭代器 - 它将通过简单地存储该集合来遍历给定集合的元素。迭代的当前索引。每次调用next()时,基本集合都会使用索引进行下标,然后递增,直到达到endIndex(您可以看到它的IndexingIterator)。

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

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

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

答案 1 :(得分:8)

我在Apple Developer中问过similar question 论坛和答案是“是的,因为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]

答案 2 :(得分:0)

在开始迭代之前已复制了Collection,并且将Foreach内部的代码应用于实际的collection,但是迭代是在复制的Collection上进行的,它将在上一次迭代后删除。