快速可变集合:找到重复的元素

Rei*_*ner 3 mutable set swift

我的应用程序使用了可变的自定义元素集。一旦我崩溃了,并发生了错误“在Set中发现重复元素”。插入后元素可能已经突变。”

在寻找解释时,我发现了这篇帖子,我对此并不完全了解。
我的印象是,不应修改集合的元素,因为这也会修改集合的哈希值,因此进一步的访问可能会失败。

我的问题:

  • 是否允许修改可变集合的元素,或者允许进行任何修改?
  • 如果不是,我是否必须首先从集合中删除该元素,然后对其进行修改,然后再将其重新插入?

编辑:

用不同的措词表述:修改可变集合的自定义元素的属性而不修改集合本身是否安全?

Mar*_*n R 6

Swift集的实现与字典类似,在“ 探索Swift字典的实现”中进行了很好的描述。特别地,元素存储是“桶”的列表,每个桶都可以被占用或不被占用。将新元素插入集合后,其哈希值将用于确定初始存储桶。如果该存储桶已被占用,则将线性搜索下一个空闲存储桶。类似地,当搜索集合中的元素时,哈希值用于确定初始存储桶,然后进行线性搜索,直到找到该元素(或未占用的存储桶)为止。

(详细信息可以在开源实现中找到,最相关的源文件是 Set.swiftNativeSet.swiftSetStorage.swiftHashTable.swift。)

更改插入元素的哈希值会破坏集合存储实现的不变性:通过元素的初始存储桶定位元素不再起作用。突变影响相等性的其他属性可能导致同一存储桶列表中出现多个“相等”元素。

因此,我认为可以肯定地说

将引用类型的实例插入集合后,不得以影响其哈希值或进行相等性测试的方式修改该实例的属性。

例子

首先,这只是引用类型集的问题值类型的集合包含该值的独立副本,并且在插入后修改该值的属性不会影响该集合:

struct Foo: Hashable {
    var x: Int
}

var set = Set<Foo>()
var foo = Foo(x: 1)
set.insert(foo)
print(set.map { $0.x })   // [1]
foo.x = 2
print(set.map { $0.x })   // [1]
set.insert(foo)
print(set.map { $0.x })   // [1, 2]
Run Code Online (Sandbox Code Playgroud)

引用类型的实例是实际对象存储的“指针”,并且修改该实例的属性不会使引用发生突变。因此,可以在将实例的属性插入集合后对其进行修改:

class Bar: Hashable {
    var x : Int

    init(x: Int) { self.x = x }

    static func == (lhs: Bar, rhs: Bar) -> Bool { return lhs.x == rhs.x }

    func hash(into hasher: inout Hasher) { hasher.combine(x) }
}

var set = Set<Bar>()
let bar = Bar(x: 1)
set.insert(bar)
print(set.map { $0.x })   // [1]
bar.x = 2
print(set.map { $0.x })   // [2]
Run Code Online (Sandbox Code Playgroud)

但是,这很容易导致崩溃,例如,如果我们再次插入相同的引用:

set.insert(bar)
Run Code Online (Sandbox Code Playgroud)
致命错误:在“集合”中发现“ Bar”类型的重复元素。
这通常意味着该类型违反了Hashable的要求,或者
这样的集合的成员在插入后发生了变异。

这是另一个示例,其中所有实例的哈希值都相同,但是修改用于相等性测试的属性会导致两个“相等”实例的集合:

class Baz: Hashable {
    var x : Int

    init(x: Int) { self.x = x }

    static func == (lhs: Baz, rhs: Baz) -> Bool { return lhs.x == rhs.x }

    func hash(into hasher: inout Hasher) { }
}

var set = Set<Baz>()
let baz1 = Baz(x: 1)
set.insert(baz1)
let baz2 = Baz(x: 2)
set.insert(baz2)
baz1.x = 2

print(set.map { $0.x })   // [2, 2]
print(set.count)             // 2
print(Set(Array(set)).count) // 1 
Run Code Online (Sandbox Code Playgroud)