swift - Swift:使用 Set.insert() 的间歇性、不一致、意外行为
问题描述
编辑:我从没想过在删除派生数据后重新启动 Xcode。现在一切都按预期工作。
我的应用程序遇到间歇性故障。我已经把它缩小到了一些奇怪的地方Set.insert()
。有时插入会导致调用我的==
函数,有时则不会,原因不明。这是我能想到的最好的精简样本;它在操场上运行。
// Results are the same whether I use Equatable or not
struct ID: Hashable, Equatable {
let idNumber: Int
// Results are the same whether I implement this or not
func hash(into hasher: inout Hasher) {
hasher.combine(idNumber)
}
// Always return true; it doesn't matter. What matters
// is that sometimes the Set.insert() doesn't even
// call this function.
static func == (_ lhs: ID, _ rhs: ID) -> Bool {
print("(eq)", terminator: ""); return true
}
}
let id0 = ID(idNumber: 0)
let id1 = ID(idNumber: 1)
var uniqueIDs = Set<ID>()
print("a", terminator: "")
uniqueIDs.insert(id0)
print("b", terminator: "")
uniqueIDs.insert(id1)
print("c", terminator: "")
如果我在操场上运行十次,大约一半的时间会eq
在输出中看到,一半的时间不会。也就是说,在尝试插入之前,大约有一半的时间Set.insert()
没有打电话给我。==
我阅读了 Swift 套装,但没有找到任何可以说明问题的东西。我有点想,如果这是预期的行为,它会被记录下来,并附上一个大的警告标志。缺少此类警告表明我在滥用Sets
,但我不知道我做错了什么。我错过了什么文档或哪个答案?
解决方案
Set
==
如果没有值冲突,则没有理由调用您的类型。我这是一个红鲱鱼。
Set
调用hash(into hasher: inout Hasher)
你的值,然后取集合内部数组大小的模数。结果是值(如果它已经存在于集合中)应该在的索引。自然地,这个过程使得多个值在散列和取模之后可以在同一个数组槽中结束。
为了弥补这一点,不是将元素直接存储在数组槽中,而是通过链表存储。从概念上讲,同一槽中的项目称为“桶”。查找元素时Set
使用哈希值找到正确的桶,但需要遍历链表才能找到确切的元素。此时,散列不再用于标识元素,因此Set
使用==
检查直到找到正确的匹配项。这通常非常有效,因为Set
应该使数组足够大,使得桶很小并且包含很少的冲突。
因为在桶中找到一个元素是O(N)
,如果你可以强制许多哈希冲突,那么你可以强制Set
的O(1)
插入/删除/检查操作退化为O(N)
对整个元素的遍历Set
(因为你可以使所有元素映射到一个桶。为了应对 DOS 漏洞,现代关联数据结构使用每次运行应用程序时随机选择的“种子”,并使用它来打乱哈希值。这样,制作具有相同哈希值的有效负载变得非常困难(这会导致存储桶过大的问题。)这就是您的不确定性的来源。请参阅PSA:stdlib 现在使用随机播种的哈希值。
从根本上说,Set<T>
实际上只是一个Dictionary
类型[T: Void]
。因此,如果您了解基于散列的关联数据结构(其他常见名称为字典、散列、散列映射等)的工作原理,大部分内容都适用。
推荐阅读
- integration - 无法向 FedEx 发送非 SOAP 费率请求
- ruby-on-rails - Docker间歇性错误“没有这样的文件或目录”
- flutter - Flutter 中 Provider 背后的模式可能是什么
- python - 定义类的部分特化
- python - 是否可以返回 tkinter 按钮的选项?
- javascript - 暗模式开关 CSS 未显示
- sql - 需要包含两个值的参数有问题
- go - Nack 消息并选择重新投递时间
- reactjs - 使用 useState 和 useEffect 钩子响应更新页面的 H1 状态显示
- sql - 如何显示没有小数和分隔符的数字?