我创建了一个采用Hashable
协议的类。
所以我创建了具有不同属性的此类的某些实例,并将它们添加到Set中。
然后我更改对象的属性。
此更改之后,集合有时会失败.contains
(也可能失败.remove
)。
在调试器检查器中,我看到该对象与Set中的元素具有相同的内存地址。那么,为什么随机失败呢?
请注意,我总能在集合中找到元素的索引。
我在(xcode10上)操场上进行了多次测试,每次执行时结果都会改变。
class Test: Hashable {
// MARK: Equatable protocol
static func == (lhs: Test, rhs: Test) -> Bool {
return lhs === rhs || lhs.hashValue == rhs.hashValue
}
var s: String
func hash(into hasher: inout Hasher) {
hasher.combine(s.hashValue)
}
init(s: String) {
self.s = s
}
}
func test(s: Set<Test>, u: Test) -> Bool {
if s.contains(u) {
print("OK")
return true
} else {
print("FAIL") // SOMETIMES FAIL
if !s.contains(u) {
if let _ = s.firstIndex(where: { $0 == u }) {
print("- OK index") // ALWAYS OK
return true
} else {
print("- FAIL index") // NEVER FAIL
return false
}
} else {
return true
}
}
}
var s: Set<Test> = []
let u1 = Test(s: "a")
s.insert(u1)
let u2 = Test(s: "b")
s.insert(u2)
test(s: s, u: u2)
u2.s = "c"
test(s: s, u: u2)
u2.s = "d"
test(s: s, u: u2)
u2.s = "b"
test(s: s, u: u2)
答案 0 :(得分:2)
集合中的内容应该是不变的。
您永远不应该将Test
对象放入集合,因为Test
是完全可变的。这就是为什么您会获得这些“奇怪且随机的”行为的原因。
调用contains
时,集合(或更确切地说是基础哈希表)将评估参数的哈希码,并查看哈希码是否与集合中的任何哈希码匹配。 (请注意,这是一个过分的简化,听起来像是O(n)操作。不是。)
在更改u2
之前,它的哈希码为x。该集合记住u2
的哈希码为x。现在,您更改u2
。现在,它具有不同的哈希码y。因此,该集合无法在其中找到哈希值为y的元素。
因此,基本上,您应该确保放入集合中的所有内容都具有恒定的哈希码。
您可以通过以下操作使Test
不可变:
let s: String
如果您想了解更多信息,可以查看如何在Swift中实现集合数据结构。我发现这个post可能也有帮助。
答案 1 :(得分:2)
从NSSet上的docs:
如果可变对象存储在集合中,则对象的哈希方法不应该依赖于可变对象的内部状态,或者当可变对象在集合中时不应对其进行修改。
>
我认为这完全涵盖了这种情况。没错,这是关于Cocoa NSSet的,但是我希望Swift Set在这方面与NSSet相对应。
仅作记录,我能够重现您描述的行为,从而消除了一些更卑鄙或更可疑的代码-不在操场上,通过==
的合法实现并使用hashValue
,而无需不必要地调用test
函数:
class Test: Equatable, Hashable, CustomStringConvertible {
var s: String
static func == (lhs: Test, rhs: Test) -> Bool {
return lhs.s == rhs.s
}
var hashValue: Int {
return s.hashValue
}
init(s: String) {
self.s = s
}
var description: String {
return "Test(\(s))"
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var s: Set<Test> = []
let u1 = Test(s: "a")
s.insert(u1)
let u2 = Test(s: "b")
s.insert(u2)
print(s.contains(u2))
u2.s = "c"
print(s.contains(u2))
}
}
要对其进行测试,请反复运行该项目。有时您会得到true true
,有时会得到true false
。不一致表示您不应该对集合中的对象进行变异。
答案 2 :(得分:-1)
由于您正在使用s
类的Test
属性来创建哈希值,因此请尝试比较s
值而不是比较对象,即
static func == (lhs: Test, rhs: Test) -> Bool
{
return lhs.s == rhs.s
}
这将解决您的问题。另外,正如我在评论中提到的那样,在失败情况下无需使用额外的if-else
。您可以简单地使用以下代码:
func test(s: Set<Test>, u: Test) -> Bool
{
if s.contains(u)
{
print("OK")
return true
}
else
{
print("FAIL: \(u.s)") // SOMETIMES FAIL
return false
}
}