缓存 Swift hash(into:) 的结果 Hashable 协议要求

Tim*_*Tim 2 hash hashable swift

我有一个类在集合和字典中被大量使用。出于性能原因,此类Hashable以旧方式实现并缓存计算的哈希值:

let hashValue: Int

init(...) {
    self.hashValue = ...
}
Run Code Online (Sandbox Code Playgroud)

在 Xcode 10.2 中,我看到一条警告,该警告hashValue已被弃用,并且很快将不再是协议要求。

令我困扰的是无论如何都无法缓存计算出的哈希值,因为hash(into:)不会返回任何内容。

func hash(into hasher: inout Hasher) {
    hasher.combine(...)
}
Run Code Online (Sandbox Code Playgroud)

考虑以下游乐场中的示例

class Class: Hashable {
    let param: Int

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

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

    public func hash(into hasher: inout Hasher) {
        print("in hash")
        hasher.combine(param)
    }
}

var dict = [Class: Int]()
let instance = Class(param: 1)
dict[instance] = 1
dict[instance] = 2
Run Code Online (Sandbox Code Playgroud)

你会看到以下日志

in hash
in hash
in hash
Run Code Online (Sandbox Code Playgroud)

我不知道为什么我们看到 3 个调用而不是 2 个,但我们确实看到了 =)。

因此,每次您使用相同的实例作为字典键或将此实例添加到集合中时,您都会收到一个新的hash(into:)调用。

在我的代码中,这样的开销非常昂贵。有人知道解决方法吗?

Ham*_*ish 6

一种选择是创建您自己的Hasher,为其提供实例的“基本组件”,然后调用finalize()以获取Int可以缓存的哈希值。

例如:

class C : Hashable {
  let param: Int

  private lazy var cachedHashValue: Int = {
    var hasher = Hasher()
    hasher.combine(param)
    // ... repeat for other "essential components"
    return hasher.finalize()
  }()

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

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

  public func hash(into hasher: inout Hasher) {
    hasher.combine(cachedHashValue)
  }
}
Run Code Online (Sandbox Code Playgroud)

对此有几点需要注意:

  • 它依赖于您的“基本组件”是不可变的,否则需要在突变时计算新的哈希值。
  • 不保证哈希值在程序执行过程中保持稳定,因此不要序列化cachedHashValue

显然,在存储单个的情况下,Int这不会那么有效,但对于更昂贵的实例,这很可能有助于提高性能。