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