在不断变化的集合中使用 for .. in 进行迭代

Chr*_*phe 6 iteration collections language-lawyer for-in-loop swift

我正在尝试使用循环对数组进行迭代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)
Run Code Online (Sandbox Code Playgroud)

这非常有效,[ "AA", "BC", "DE", "FG" ]即使在第一次迭代后数组完全更改为对值进行迭代["AAXX", "bc", "KLM"]

我想知道我是否可以放心地依赖这种行为。不幸的是,语言指南没有说明修改集合迭代集合的任何信息。该for .. in部分也没有解决这个问题。所以:

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

dea*_*.dg 5

IteratorProtocol的文档说:“每当您对数组、集合或任何其他集合或序列使用 for-in 循环时,您\xe2\x80\x99 就在使用该类型\xe2\x80\x99s 迭代器。” 因此,我们保证for in将使用.makeIterator()和分别.next()最常见地定义在Sequence和上的循环IteratorProtocol

\n

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

\n

同样的警告不会出现在Collection的文档中,但我也不认为迭代器会生成副本,因此我不认为在迭代集合时修改集合通常是,安全的。

\n

但是,Swift 中的大多数集合类型都是struct具有值语义或写时复制语义的。我不太确定这方面的文档在哪里,但这个链接确实说“在 Swift 中,ArrayString、 和Dictionary都是值类型......你不需要做任何特殊的 \xe2\ x80\x94,例如制作显式副本 \xe2\x80\x94 以防止其他代码在您背后修改该数据。” 特别是,这意味着 for Array,.makeIterator() 无法保存对数组的引用,因为 for 迭代器Array不必“做任何特殊的事情”来防止其他代码(即您的代码)修改它保存的数据。

\n

我们可以更详细地探讨这一点。Iterator的类型Array定义type IndexingIterator<Array<Element>>。文档IndexingIterator说它是集合迭代器的默认实现,因此我们可以假设大多数集合都会使用它。我们可以在源代码IndexingIterator中看到它保存了一个副本集合的

\n
@frozen\npublic struct IndexingIterator<Elements: Collection> {\n  @usableFromInline\n  internal let _elements: Elements\n  @usableFromInline\n  internal var _position: Elements.Index\n\n  @inlinable\n  @inline(__always)\n  /// Creates an iterator over the given collection.\n  public /// @testable\n  init(_elements: Elements) {\n    self._elements = _elements\n    self._position = _elements.startIndex\n  }\n  ...\n}\n
Run Code Online (Sandbox Code Playgroud)\n

默认情况下.makeIterator()只是创建此副本。

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

尽管您可能不想信任此源代码,但库演化文档声称“该@inlinable属性是库开发人员的承诺,即函数的当前定义在与库的未来版本一起使用时将保持正确”,@frozen并且意味着 的成员IndexingIterator不能改变。

\n

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

\n

综上所述

\n

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

\n

然而,有足够的文档表明循环for in只是调用.makeIterator(),并且对于具有值语义和默认迭代器类型(例如,Array)的任何集合,.makeIterator()都会创建一个副本,因此不会受到循环内代码的影响。此外,因为Array和一些其他类型,如SetDictionary写时复制的,所以在循环内修改这些集合将产生一次性复制惩罚,因为循环体不会对其存储具有唯一的引用(因为迭代器将也有参考意义)。如果您没有对存储的唯一引用,这与在循环外修改集合所产生的惩罚完全相同。

\n

如果没有这些假设,就无法保证您的安全,但在某些情况下您仍然可能拥有安全。

\n

编辑:

\n

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

\n
import Foundation\n\n/// This is clearly fine and works as expected.\nprint("Test normal")\nfor _ in 0...10 {\n    let x: NSMutableArray = [0,1,2,3]\n    for i in x {\n        print(i)\n    }\n}\n\n/// This is also okay. Reassigning `x` does not mutate the reference that the iterator holds.\nprint("Test reassignment")\nfor _ in 0...10 {\n    var x: NSMutableArray = [0,1,2,3]\n    for i in x {\n        x = []\n        print(i)\n    }\n}\n\n/// This crashes. The iterator assumes that the last index it used is still valid, but after removing the objects, there are no valid indices.\nprint("Test removal")\nfor _ in 0...10 {\n    let x: NSMutableArray = [0,1,2,3]\n    for i in x {\n        x.removeAllObjects()\n        print(i)\n    }\n}\n\n/// This also crashes. `.enumerated()` gets a reference to `x` which it expects will not be modified behind its back.\nprint("Test removal enumerated")\nfor _ in 0...10 {\n    let x: NSMutableArray = [0,1,2,3]\n    for i in x.enumerated() {\n        x.removeAllObjects()\n        print(i)\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n