【问题标题】:problems with set on swift language设置 swift 语言的问题
【发布时间】:2018-10-24 12:26:42
【问题描述】:

我创建了一个采用Hashable 协议的类。

所以我创建了这个类的一些具有不同属性的实例并将它们添加到集合中。

然后我改变一个对象的属性。

在此更改之后,Set 有时会失败 .contains(还有 .remove)。 在调试器检查器中,我看到该对象与 Set 中的元素具有相同的内存地址。那么为什么会随机失败呢? 请注意,我总能找到集合内元素的索引。

我在 Playground(在 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)

【问题讨论】:

  • 为什么在else 子句中使用嵌套的if !s.contains(u)?你能解释一下你想达到什么目的吗/
  • 对不起,嵌套的if是错误的,但不影响问题
  • 可能与stackoverflow.com/questions/28461675/…重复,经过必要的修改后
  • 一种解决方法可能是:将哈希存储在私有属性 private var _hash:Int? 中,并且在 hashValue 中只存储一次:var hashValue: Int { if (_hash == nil) { _hash = s.hashValue }; return _hash! }(为了线程安全,使用 GCD once 东西)跨度>

标签: swift xcode set


【解决方案1】:

来自 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。不一致表明您不应该改变集合中的对象。

【讨论】:

  • 我尝试实现 hashValue 而不是 hash 方法返回 s 属性的哈希值(所以不要使用 Haser)但不要' teseolve class Test: Hashable { static func == (lhs: Test, rhs: Test) -&gt; Bool { return lhs.hashValue == rhs.hashValue } var hashValue: Int { return s.hashValue } var s: String init(s: String) { self.s = s } }
  • 好的,抱歉。我发现文档显示 Sweeper 的答案是正确的。
【解决方案2】:

集合中的东西应该是不可变的。

您永远不应该将Test 对象放入集合中,因为Test 是完全可变的。这正是你得到这些“奇怪和随机”行为的原因。

当您调用contains 时,集合(或者更确切地说,底层哈希表)评估参数的哈希码并查看哈希码是否与集合中的任何哈希码匹配。 (请注意,这过于简单化了,这听起来像是一个 O(n) 操作。事实并非如此。)

在您更改u2 之前,它的哈希码为x。该集合记得u2 的哈希码为 x。现在你更改u2。它现在有一个不同的哈希码 y。因此,该集合无法在其中找到哈希码为 y 的元素。

因此,基本上,您应该确保放入集合中的所有内容都具有恒定的哈希码。

您可以通过以下方式使Test 不可变:

let s: String

如果您想了解更多信息,可以查看 set 数据结构在 Swift 中是如何实现的。我发现这个post 也可能有帮助。

【讨论】:

  • 您的回答可能是正确的(至少,对于 Java 部分,它是正确的),但我肯定希望 NS(Set) 文档包含有关此行​​为的提示......至少,我希望行为不是不确定的。
  • @AndreasOetjen 我查看了 Swift Set 文档,但找不到任何东西。如果您查看NSSet.contains,它甚至表明该操作是 O(n)。 Apple 文档还不够好……
  • 有趣的是,它似乎以某种方式依赖于 Hasher 的种子:如果你在一个循环中运行整个事情,比如说 100 次(例如 100x 创建一个新集合,添加新对象,修改它们,测试它们),然后一个程序运行你得到 100 倍“OK”,下一次运行,你得到 100 倍“失败”(Hasher 每次程序运行时都会改变它的种子),但绝不会在 OK 之间混合和失败。
  • 嗯,好的。我认为通过引用管理的对象在 Set 中是同步的。显然,为了优化代码,Set 以某种方式将其元素的哈希存储在缓存中,而无需不断更新它。奇怪的是,有时该方法有效,有时无效。在我的测试中,碰巧在同一次执行中,一些测试失败了,而另一些则没有......
  • @Sweeper 找到的文档表明你绝对是正确的。
【解决方案3】:

由于您使用Test 类的s 属性来创建哈希值,请尝试比较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
    }
}

【讨论】:

  • 不,只测试 s 的值并不能解决问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-10
  • 1970-01-01
  • 1970-01-01
  • 2023-03-13
  • 2014-09-05
相关资源
最近更新 更多