用 for .. in 迭代一个不断变化的集合

Iterating with for .. in on a changing collection

我正在尝试使用 for .. in .. 循环对数组进行迭代。我的问题是关于在循环体内改变集合的情况。

看起来迭代是安全的,即使列表同时收缩。 for 迭代变量连续获取循环开始时数组中的(索引和)元素的值,尽管对流进行了更改。示例:

var slist = [ "AA", "BC", "DE", "FG" ]

for (i, st) in slist.enumerated() {   // for st in slist gives a similar result
    print ("Index \(i): \(st)")
    if st == "AA" {    // at one iteration change completely the list
        print (" --> check 0: \(slist[0]), and 2: \(slist[2])")
        slist.append ("KLM") 
        slist.insert(st+"XX", at:0)   // shift the elements in the array
        slist[2]="bc"                 // replace some elements to come
        print (" --> check again 0: \(slist[0]), and 2: \(slist[2])")
        slist.remove(at:3)
        slist.remove(at:3)
        slist.remove(at:1)            // makes list shorter
    }
}
print (slist)

这非常有效,即使在第一次迭代后数组完全更改为 ["AAXX", "bc", "KLM"]

,也会对值 [ "AA", "BC", "DE", "FG" ] 进行迭代

我想知道我是否可以安全地依赖这种行为。不幸的是,语言指南没有说明 iterating on a collection when the collection is modified. And the for .. in section 也没有解决这个问题。所以:

  1. 我可以安全地依赖语言规范中提供的关于此迭代行为的保证吗?
  2. 或者我只是幸运地使用了 Swift 5.4 的当前版本?在这种情况下,语言规范中是否有任何不能想当然的线索?与索引迭代相比,此迭代行为(例如某些副本)是否存在性能开销?

slist.enumerate() 创建新实例 EnumeratedSequence<[String]>

To create an instance of EnumeratedSequence, call enumerated() on a sequence or collection. The following example enumerates the elements of an array. reference

如果删除 .enumerate() 会产生相同的结果,任何 st 都具有旧值。发生这种情况是因为 for-in loop 生成了 IndexingIterator<[String]>.

的新实例

Whenever you use a for-in loop with an array, set, or any other collection or sequence, you’re using that type’s iterator. Swift uses a sequence’s or collection’s iterator internally to enable the for-in loop language construct. reference

关于问题:

  • 您将能够删除所有元素并仍然执行循环安全,因为生成了一个新实例来执行交互。
  • Swift 在内部使用迭代器来启用 for-in 然后就没有开销可以比较了。从逻辑上讲,数组越大,性能会受到影响。

IteratorProtocol 的文档说“无论何时,当你对数组、集合或任何其他集合或序列使用 for-in 循环时,你都在使用该类型的迭代器。”因此,我们可以保证 for in 循环将使用 .makeIterator().next(),它们通常分别在 SequenceIteratorProtocol 上定义。

Sequence 的文档说“Sequence 协议不要求符合类型是否会被迭代破坏性地消耗。”因此,这意味着不需要 Sequence 的迭代器来制作副本,因此我认为在遍历序列时修改序列通常是不安全的。

Collection 的文档中没有出现同样的警告,但我也不认为迭代器会生成副本有任何保证,因此我不认为在修改集合的同时一般来说,对其进行迭代是安全的。

但是,Swift 中的大多数集合类型都是具有值语义或写时复制语义的 struct。我不太确定这方面的文档在哪里,但是 this link 确实说“在 Swift、ArrayStringDictionary 中都是值类型......你不需要做任何特殊的事情——比如制作一个明确的副本——来防止其他代码在你背后修改该数据。”特别是,这意味着对于 Array.makeIterator() 不能 持有对数组的引用,因为 Array 的迭代器不必“做任何特殊的东西”以防止其他代码(即您的代码)修改它保存的数据。

我们可以更详细地探讨这个问题。 ArrayIterator 类型对于 IndexingIteratordefined as type IndexingIterator<Array<Element>>. The documentation IndexingIterator says that it is the default implementation of the iterator for collections, so we can assume that most collections will use this. We can see in the source code,它拥有其集合

副本
@frozen
public struct IndexingIterator<Elements: Collection> {
  @usableFromInline
  internal let _elements: Elements
  @usableFromInline
  internal var _position: Elements.Index

  @inlinable
  @inline(__always)
  /// Creates an iterator over the given collection.
  public /// @testable
  init(_elements: Elements) {
    self._elements = _elements
    self._position = _elements.startIndex
  }
  ...
}

并且默认 .makeIterator() 只是创建此副本。

extension Collection where Iterator == IndexingIterator<Self> {
  /// Returns an iterator over the elements of the collection.
  @inlinable // trivial-implementation
  @inline(__always)
  public __consuming func makeIterator() -> IndexingIterator<Self> {
    return IndexingIterator(_elements: self)
  }
}

虽然您可能不想信任此源代码,但 library evolution 的文档声称“@inlinable 属性是库开发人员的承诺,即函数的当前定义将保留与库的未来版本一起使用时正确”,@frozen 也意味着 IndexingIterator 的成员不能更改。

总而言之,这意味着任何具有值语义和 IndexingIterator 作为其 Iterator 的集合类型在使用 [=13 时必须 进行复制=] 循环(至少直到下一个 ABI 中断,这应该是一个很长的路要走)。即便如此,我认为 Apple 也不太可能改变这种行为。

总结

我不知道文档中有任何地方明确指出“您可以在迭代数组时修改数组,迭代将像复制一样进行”,但这也是一种可能不应该写下来的语言,因为编写这样的代码肯定会使初学者感到困惑。

然而,有足够的文档说 for in 循环只调用 .makeIterator() 并且对于任何具有值语义和默认迭代器类型的集合(例如 Array), .makeIterator() 进行复制,因此不受循环内代码的影响。此外,因为 Array 和其他一些类型如 SetDictionary 是写时复制,在循环内修改这些集合将有一次性复制惩罚,因为循环不会对其存储有唯一引用(因为迭代器会有)。如果您没有对存储的唯一引用,这与在循环外使用 have 修改集合的惩罚完全相同。

如果没有这些假设,您将无法保证安全,但在某些情况下您可能仍然会遇到安全问题。

编辑:

我刚刚意识到我们可以创建一些对序列不安全的情况。

import Foundation

/// This is clearly fine and works as expected.
print("Test normal")
for _ in 0...10 {
    let x: NSMutableArray = [0,1,2,3]
    for i in x {
        print(i)
    }
}

/// This is also okay. Reassigning `x` does not mutate the reference that the iterator holds.
print("Test reassignment")
for _ in 0...10 {
    var x: NSMutableArray = [0,1,2,3]
    for i in x {
        x = []
        print(i)
    }
}

/// This crashes. The iterator assumes that the last index it used is still valid, but after removing the objects, there are no valid indices.
print("Test removal")
for _ in 0...10 {
    let x: NSMutableArray = [0,1,2,3]
    for i in x {
        x.removeAllObjects()
        print(i)
    }
}

/// This also crashes. `.enumerated()` gets a reference to `x` which it expects will not be modified behind its back.
print("Test removal enumerated")
for _ in 0...10 {
    let x: NSMutableArray = [0,1,2,3]
    for i in x.enumerated() {
        x.removeAllObjects()
        print(i)
    }
}

这是一个 NSMutableArray 这一事实很重要,因为这种类型具有引用语义。由于 NSMutableArray 符合 Sequence,我们知道在迭代序列时改变序列是不安全的,即使使用 .enumerated().