当与无效的迭代器/索引一起使用时,swift 集合的安全性如何?

How safe are swift collections when used with invalidated iterators / indices?

我在 swift stdlib 参考中没有看到很多信息。例如,Dictionary 说某些方法(如删除)会使索引无效,但仅此而已。

要使一种语言能够自我调用 "safe",它需要解决经典 C++ 脚枪的问题:

  1. 获取指向向量中元素的指针,然后添加更多元素(指针现在无效),现在使用指针,崩溃

  2. 开始遍历集合。在迭代时,删除一些元素(在当前迭代器位置之前或之后)。继续迭代,崩溃。

(编辑:在 C++ 中,你 幸运 崩溃 - 更糟糕的情况是内存损坏)

我相信 1 是由 swift 解决的,因为如果集合存储 类,对元素进行引用(例如强指针)将增加引用计数。但是,我不知道 2 的答案。

如果在 c++ 中比较 are/are 没有被 swift 解决的步兵枪,那将是非常有用的。

编辑,由于 Robs 的回答:

似乎确实发生了一些未记录的类似快照的行为 与字典 and/or for 循环。迭代创建快照/隐藏 启动时的副本。

这给了我很大的 "WAT" 和 "cool, that's sort of safe, I guess",以及 "how expensive is this copy?"。

我没有在 Generator 或 for-loop 中看到这个记录。

下面的代码打印字典的两个逻辑快照。首先 snapshot 是 userInfo 因为它是在迭代循环的开始,并且确实 不反映循环期间所做的任何修改。

var userInfo: [String: String] = [
    "first_name" : "Andrei",
    "last_name" : "Puni",
    "job_title" : "Mad scientist"
]

userInfo["added_one"] = "1"  // can modify because it's var

print("first snapshot:")
var hijacked = false
for (key, value) in userInfo {
    if !hijacked {
        userInfo["added_two"] = "2"  // doesn't error     
        userInfo.removeValueForKey("first_name")  // doesn't error
        hijacked = true
    }
    print("- \(key): \(value)")
}

userInfo["added_three"] = "3" // modify again

print("final snapshot:")
for (key, value) in userInfo {
    print("- \(key): \(value)")
}

如您所说,#1 不是问题。您没有指向 Swift 中对象的指针。您要么拥有它的价值,要么拥有对它的引用。如果你有它的价值,那么它就是一个副本。如果您有参考,那么它会受到保护。所以这里没有问题。

但是让我们考虑第二个和实验,感到惊讶,然后停止惊讶。

var xs = [1,2,3,4]

for x in xs { // (1)
    if x == 2 {
        xs.removeAll() // (2)
    }
    print(x) // Prints "1\n2\n3\n\n"
}

xs // [] (3)

等等,当我们吹掉(2)处的值时,它如何打印所有值。我们现在很惊讶。

但我们不应该。 Swift 个数组是 个值 。 (1) 处的 xs 是一个值。没有什么能改变它。这不是 "a pointer to memory that includes an array structure that contains 4 elements.",而是 [1,2,3,4]。在 (2) 处,我们不 "remove all elements from the thing xs pointed to." 我们取 xs ,创建一个数组,如果您删除所有元素(即 [] 在所有情况下),然后将该新数组分配给 xs。没有什么不好的事情发生。

那么文档中的 "invalidates all indices?" 是什么意思呢?正是这个意思。如果我们生成索引,它们就不再有用了。让我们看看:

var xs = [1,2,3,4]

for i in xs.indices {
    if i == 2 {
        xs.removeAll()
    }
    print(xs[i]) // Prints "1\n2\n" and then CRASH!!!
}

一旦 xs.removeAll() 被调用,xs.indices 的旧结果将不再具有任何意义。您不能针对它们来自的集合安全地使用这些索引。

Swift 中的

"Invalidates indices" 与 C++ 中的 "invalidates iterators." 不同我认为这很安全,除了使用集合索引总是有点危险,所以你应该尽可能避免索引集合;迭代它们。即使您出于某种原因需要索引,也可以使用 enumerate 获取它们而不会产生任何索引危险。

(旁注,dict["key"] 没有索引到 dict。字典有点混乱,因为它们的键不是索引。通过 DictionaryIndex 索引访问字典就像通过 Int 索引访问数组是危险的。)

另请注意,上述内容不适用于 NSArray。如果您在迭代时修改 NSArray,您将收到 "mutated collection while iterating" 错误。我只讨论 Swift 数据类型。


编辑:for-in 的工作原理是 very explicit

The generate() method is called on the collection expression to obtain a value of a generator type—that is, a type that conforms to the GeneratorType protocol. The program begins executing a loop by calling the next() method on the stream. If the value returned is not None, it is assigned to the item pattern, the program executes the statements, and then continues execution at the beginning of the loop. Otherwise, the program does not perform assignment or execute the statements, and it is finished executing the for-in statement.

返回的 Generator 是一个 struct 并且包含一个集合值。您不会期望对某些其他值进行任何更改来修改其行为。请记住:[1,2,3]4 没有什么不同。他们都是价值观。当您分配给他们时,他们会制作副本。因此,当您在集合值上创建一个生成器时,您将对该值进行快照,就像我在数字 4 上创建一个生成器一样。(这引发了一个有趣的问题,因为生成器并不是真正的值,所以真的不应该是结构。它们应该是 类。Swift stdlib 一直在修复这个问题。例如,请参阅新的 AnyGenerator。但它们仍然包含一个数组值,你永远不会想到更改一些其他数组值以影响它们。)

另请参阅 "Structures and Enumerations Are Value Types",其中详细介绍了 Swift 中值类型的重要性。数组只是结构。

是的,这意味着存在逻辑复制。 Swift 进行了许多优化,以在不需要时最大程度地减少实际复制。在您的情况下,当您在迭代字典时对其进行变异时,这将强制进行复制。如果您是特定值的后备存储的唯一消费者,则变更很便宜。但如果你不是,那就是 O(n)。 (这由 Swift 内置 isUniquelyReferenced() 决定。)长话短说:Swift 集合是写时复制的,简单地传递数组不会导致分配实际内存或已复制。

您不会免费获得 COW。您自己的结构 不是 COW。这是 Swift 在 stdlib 中所做的事情。 (请参阅 Mike Ash 关于如何重新创建它的 great discussion。)传递您自己的自定义结构会导致真正的副本发生。也就是说,大多数结构中的大部分内存都存储在集合中,而这些集合是 COW,因此复制结构的成本通常很小。

这本书并没有花很多时间深入研究 Swift 中的值类型(它解释了一切;它只是没有一直说 "hey, and this is what that implies")。另一方面,这是 WWDC 上不变的话题。可能你特别感兴趣Building Better Apps with Value Types in Swift which is all about this topic. I believe Swift in Practice也讨论一下。


编辑 2:

@KarlP 在下面的评论中提出了一个有趣的观点,值得一提。 None 我们正在讨论的价值安全承诺与 for-in 有关。它们基于 Arrayfor-in 完全不承诺如果在迭代过程中改变集合会发生什么。那甚至没有意义。 for-in 不 "iterate over collections," 它在 Generators 上调用 next()。因此,如果您的 Generator 在集合更改后变为未定义,那么 for-in 将爆炸,因为 Generator 爆炸了。

这意味着以下内容可能不安全,具体取决于您阅读规范的严格程度:

func nukeFromOrbit<C: RangeReplaceableCollectionType>(var xs: C) {
    var hijack = true
    for x in xs {
        if hijack {
            xs.removeAll()
            hijack = false
        }
        print(x)
    }
}

并且编译器不会在这里帮助您。它适用于所有 Swift 集合。但是,如果在 您的 集合的变异后调用 next() 是未定义的行为,那么这就是未定义的行为。

我的意见是,在这种情况下,创建一个允许其 Generator 变为未定义的集合会很糟糕 Swift。如果你这样做,你甚至可以争辩说你已经破坏了 Generator 规范(它不提供 UB "out" 除非生成器已被复制或已返回 nil)。所以你可能会争辩说上面的代码完全符合规范并且你的生成器坏了。这些论点对于像 Swift 这样的 "spec" 来说有点混乱,它没有深入到所有极端情况。

这是否意味着您可以在 Swift 中编写不安全的代码而不会得到明确的警告?绝对地。但在许多通常会导致现实世界错误的情况下,Swift 的内置行为会做正确的事情。在这一点上,它比其他一些选项更安全。