我在swift stdlib引用中没有看到很多信息。例如,Dictionary说某些方法(比如删除)会使索引无效,但就是这样。
对于一种称自己为“安全”的语言,它需要一个经典C ++脚注的解决方案:
获取向量中元素的指针,然后添加更多元素(指针现在无效),现在使用指针,崩溃
开始迭代集合。迭代时,删除一些元素(在当前迭代器位置之前或之后)。继续迭代,崩溃。
(编辑:在c ++中,你幸运崩溃 - 更糟糕的是内存损坏)
我相信1是由swift解决的,因为如果集合存储类,则对元素进行引用(例如强指针)会增加引用计数。但是,我不知道2的答案。
如果在c ++中对脚ns进行了比较,那么它将非常有用。
编辑,由于Robs回答:看起来确实存在一些未记录的类似快照的行为 使用Dictionary和/或for循环。迭代创建快照/隐藏 它开始时的副本。
这给了我一个很大的“WAT”和“酷,那种安全,我猜”,“这个副本有多贵?”。
我没有在Generator或for-loop中看到这个记录。
以下代码打印字典的两个逻辑快照。首先
快照是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)")
}
答案 0 :(得分:8)
正如你所说,#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\4\n"
}
xs // [] (3)
等等,当我们吹走(2)处的值时,它如何打印所有值。我们现在非常惊讶。
但我们不应该。 Swift数组是值。 ({1}}处的xs
是一个值。什么都不能改变它。它不是"指向内存的指针,包含一个包含4个元素的数组结构。"它是值 [1,2,3,4]
。在(2),我们不会删除指向xs
的所有元素。"我们接受xs 是的事情,创建一个数组,如果你删除了所有元素(在所有情况下都是[]
),然后将新数组分配给{{1} }。没有什么不好的事情发生。
那么文档的意思是"使所有索引无效?"这意味着什么。如果我们生成指数,他们就不再有好处了。我们来看看:
xs
调用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()
的旧结果不再具有任何意义。您不得安全地使用这些索引来对付它们来自的集合。
"索引无效"在Swift中与C ++"使迭代器无效是不一样的。"我之所以称之为非常安全,除了使用集合索引总是有点危险这样的事实,所以当你可以帮助它时你应该避免索引集合;反而替代它们。即使您出于某种原因需要索引,也可以使用xs.indices
来获取索引,而不会产生索引的任何危险。
(旁注,enumerate
没有索引到dict["key"]
。字典有点混乱,因为它们的键不是它们的索引。通过dict
索引访问字典同样危险通过DictionaryIndex
索引访问数组。)
另请注意,上述内容并不适用于Int
。如果您在迭代时修改NSArray
,那么在迭代时会得到一个变异的集合"错误。我只讨论Swift数据类型。
编辑:NSArray
的工作原理为very explicit:
在集合表达式上调用generate()方法以获取生成器类型的值 - 即符合GeneratorType协议的类型。程序通过调用流上的next()方法开始执行循环。如果返回的值不是None,则将其分配给项模式,程序执行语句,然后在循环开始时继续执行。否则,程序不执行赋值或执行语句,并且它已完成执行for-in语句。
返回的for-in
是Generator
并包含一个集合值。您不希望任何其他值的更改来修改其行为。请注意:struct
与[1,2,3]
没有区别。他们都是两个价值观。分配它们时,它们会复制。因此,当你在一个集合值上创建一个Generator时,你就会对该值进行快照,就像我在数字4上创建一个Generator一样。(这引发了一个有趣的问题,因为生成器并不是真的有价值,所以不应该是结构。它们应该是类.Swift stdlib一直在修复它。比如看新的4
。但是它们仍然包含一个数组值,你永远不会期望更改一些其他数组值会影响它们。)
另请参阅"Structures and Enumerations Are Value Types",其中详细介绍了Swift中值类型的重要性。数组只是结构。
是的,这意味着有逻辑上的复制。 Swift有许多优化可以在不需要时最小化实际复制。在您的情况下,当您在迭代字典时改变字典时,这将强制复制发生。如果您是特定价值的后备存储的唯一消费者,那么变异是便宜的。但如果你不是,那就是O(n)。 (这由Swift内置AnyGenerator
确定。)长话短说:Swift Collections是Copy-on-Write,只是传递数组不会导致分配或复制真实内存。
你没有免费获得COW。您自己的结构不 COW。这是Swift在stdlib中所做的事情。 (请参阅Mike Ash的great discussion了解如何重新创建它。)传递自己的自定义结构会导致真正的副本发生。也就是说,大多数结构中的大部分内存存储在集合中,而那些集合是COW,因此复制结构的成本通常很小。
这本书并没有花费大量时间在Swift中钻取价值类型(它解释了一切;它只是不会继续说"嘿,这就是这意味着&#34 )。另一方面,它是WWDC的常头。您可能会对Building Better Apps with Value Types in Swift感兴趣,这是关于此主题的全部内容。我相信Swift in Practice也讨论了它。
EDIT2:
@KarlP在下面的评论中提出了一个有趣的观点,值得解决。我们讨论的价值安全承诺都与isUniquelyReferenced()
无关。他们基于for-in
。 Array
根本没有任何承诺,如果你在迭代过程中突变了一个集合会发生什么。那甚至没有意义。 for-in
没有对集合进行迭代,"它在for-in
上调用next()
。因此,如果您的Generators
在收集更改后变为未定义,那么Generator
将会爆炸,因为for-in
已爆炸。
这意味着以下内容可能不安全,具体取决于您阅读规范的严格程度:
Generator
编译器在这里没有帮助你。它适用于所有Swift系列。但是如果在你的集合的变异之后调用func nukeFromOrbit<C: RangeReplaceableCollectionType>(var xs: C) {
var hijack = true
for x in xs {
if hijack {
xs.removeAll()
hijack = false
}
print(x)
}
}
是未定义的行为,那么这是未定义的行为。
我的观点是,在这种情况下,制作一个允许其next()
未定义的集合会很糟糕。如果你这样做,你甚至可以说你已经破坏了Generator
规范(它没有提供UB&#34; out&#34;除非生成器被复制或返回为零)。所以你可以说上面的代码完全符合规范,你的发电机坏了。对于&#34; spec&#34;这些论点往往有些混乱。像斯威夫特那样,并没有深入到所有角落的情况。
这是否意味着您可以在Swift中编写不安全的代码而不会得到明确的警告?绝对。但在很多情况下,通常会导致真实的错误,Swift的内置行为是正确的。在这方面,它比其他一些选择更安全。