如何在 Swift 中为 Int 数组(自定义字符串结构)实现 Hashable Protocol

How to implement the Hashable Protocol in Swift for an Int array (a custom string struct)

我正在制作一个类似于 String 的结构,只是它只处理 Unicode UTF-32 标量值。因此,它是一个 UInt32 的数组。 (有关更多背景信息,请参阅 。)

我想做什么

我希望能够使用我的自定义 ScalarString 结构作为字典中的键。例如:

var suffixDictionary = [ScalarString: ScalarString]() // Unicode key, rendered glyph value

// populate dictionary
suffixDictionary[keyScalarString] = valueScalarString
// ...

// check if dictionary contains Unicode scalar string key
if let renderedSuffix = suffixDictionary[unicodeScalarString] {
    // do something with value
}

问题

为了做到这一点,ScalarString 需要实施 Hashable Protocol。我以为我可以做这样的事情:

struct ScalarString: Hashable {

    private var scalarArray: [UInt32] = []

    var hashValue : Int {
        get {
            return self.scalarArray.hashValue // error
        }
    }
}

func ==(left: ScalarString, right: ScalarString) -> Bool {
    return left.hashValue == right.hashValue
}

但后来我发现 Swift arrays 没有 hashValue

我读了什么

文章 Strategies for Implementing the Hashable Protocol in Swift 有很多很棒的想法,但我没有看到任何在这种情况下看起来会很好的想法。具体来说,

这是我读到的一些其他内容:

问题

Swift 字符串有一个 hashValue 属性,所以我知道这是可以做到的。

如何为我的自定义结构创建 hashValue

更新

更新 1: 我想做一些不涉及转换为 String 然后使用 StringhashValue .我制作自己的结构的全部目的是为了避免进行大量 String 转换。 String 从某处获取 hashValue。看来我可以用同样的方法得到它。

更新 2: 我一直在研究其他上下文中字符串哈希码算法的实现。不过,我很难知道哪个是最好的并用 Swift 来表达它们。

更新 3

我宁愿不导入任何外部框架,除非这是完成这些事情的推荐方法。

我提交了一个使用 DJB 哈希函数的可能解决方案。

一个建议 - 由于您正在为 String 建模,将 [UInt32] 数组转换为 String 并使用 StringhashValue?像这样:

var hashValue : Int {
    get {
        return String(self.scalarArray.map { UnicodeScalar([=10=]) }).hashValue
    }
}

这也可以让您方便地将自定义 structString 进行比较,尽管这是否是个好主意取决于您尝试做什么...

另请注意,使用这种方法,如果 String 的表示在规范上等价,ScalarString 的实例将具有相同的 hashValue,这可能是您想要的,也可能不是您想要的.

所以我想如果您希望 hashValue 代表唯一的 String,我的方法会很好。如果您希望 hashValue 代表一个唯一的 UInt32 值序列,@Kametrixom 的答案就是要走的路...

这不是一个非常优雅的解决方案,但效果很好:

"\(scalarArray)".hashValue

scalarArray.description.hashValue

它只使用文本表示作为哈希源

编辑(2017 年 5 月 31 日):请参阅已接受的答案。这个答案 几乎只是关于如何使用 CommonCrypto 框架

的演示

好的,我继续使用 CommonCrypto 框架中的 SHA-256 散列算法,使用 Hashable 协议扩展了所有数组。你必须把

#import <CommonCrypto/CommonDigest.h>

到你的桥接头中让它工作。遗憾的是,必须使用指针:

extension Array : Hashable, Equatable {
    public var hashValue : Int {
        var hash = [Int](count: Int(CC_SHA256_DIGEST_LENGTH) / sizeof(Int), repeatedValue: 0)
        withUnsafeBufferPointer { ptr in
            hash.withUnsafeMutableBufferPointer { (inout hPtr: UnsafeMutableBufferPointer<Int>) -> Void in
                CC_SHA256(UnsafePointer<Void>(ptr.baseAddress), CC_LONG(count * sizeof(Element)), UnsafeMutablePointer<UInt8>(hPtr.baseAddress))
            }
        }

        return hash[0]
    }
}

编辑(2017 年 5 月 31 日):不要这样做,即使 SHA256 几乎没有散列冲突,但通过散列相等性定义相等性是错误的想法

public func ==<T>(lhs: [T], rhs: [T]) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

这与 CommonCrypto 一样好。它很丑,但速度很快,而且不多肯定几乎没有哈希冲突

编辑(2015 年 7 月 15 日):我刚刚做了一些速度测试:

大小为 n 的随机填充 Int 数组平均运行超过 1000 次

n      -> time
1000   -> 0.000037 s
10000  -> 0.000379 s
100000 -> 0.003402 s

而使用字符串散列方法:

n      -> time
1000   -> 0.001359 s
10000  -> 0.011036 s
100000 -> 0.122177 s

所以 SHA-256 方式比字符串方式快 33 倍左右。我并不是说使用字符串是一个很好的解决方案,但这是我们现在唯一可以比较的解决方案

更新

马丁 writes:

As of Swift 4.1, the compiler can synthesize Equatable and Hashable for types conformance automatically, if all members conform to Equatable/Hashable (SE0185). And as of Swift 4.2, a high-quality hash combiner is built-in into the Swift standard library (SE-0206).

Therefore there is no need anymore to define your own hashing function, it suffices to declare the conformance:

struct ScalarString: Hashable, ... {

    private var scalarArray: [UInt32] = []

    // ... }

因此,下面的答案需要重写(再次)。在此之前,请参考上面 link 中 Martin R 的回答。


旧答案:

这个答案在提交我的 original answer to code review 后被完全重写了。

如何实现 Hashable 协议

Hashable protocol 允许您使用自定义 class 或结构作为字典键。为了实施此协议,您需要

  1. 实现Equatable protocol(Hashable继承自Equatable)
  2. Return 一个计算 hashValue

这些要点来自文档中给出的公理:

x == y implies x.hashValue == y.hashValue

其中 xy 是某些类型的值。

实施 Equatable 协议

为了实现 Equatable 协议,您定义您的类型如何使用 ==(等价)运算符。在您的示例中,可以这样确定等效性:

func ==(left: ScalarString, right: ScalarString) -> Bool {
    return left.scalarArray == right.scalarArray
}

== 函数是全局函数,因此它超出了您的 class 或结构。

Return 一个计算的 hashValue

您的自定义 class 或结构还必须具有计算的 hashValue 变量。一个好的散列算法会提供范围广泛的散列值。但是需要注意的是,你不需要保证哈希值都是唯一的。当两个不同的值具有相同的散列值时,这称为散列冲突。当发生碰撞时它需要一些额外的工作(这就是为什么需要良好的分布),但一些碰撞是可以预料的。据我了解, == 函数完成了额外的工作。 (更新:)

计算散列值的方法有很多种。例如,您可以做一些简单的事情,例如返回数组中的元素数。

var hashValue: Int {
    return self.scalarArray.count
} 

每当两个数组具有相同数量的元素但不同的值时,这将导致散列冲突。 NSArray 显然使用了这种方法。

DJB 哈希函数

处理字符串的常见哈希函数是 DJB 哈希函数。这是我将要使用的,但请查看其他一些 here

Swift 实现 provided by @MartinR 如下:

var hashValue: Int {
    return self.scalarArray.reduce(5381) {
        ([=13=] << 5) &+ [=13=] &+ Int()
    }
}

这是我最初实现的改进版本,但让我也包括旧的扩展形式,这对于不熟悉 reduce 的人来说可能更具可读性。我相信这是等价的:

var hashValue: Int {

    // DJB Hash Function
    var hash = 5381

    for(var i = 0; i < self.scalarArray.count; i++)
    {
        hash = ((hash << 5) &+ hash) &+ Int(self.scalarArray[i])
    }

    return hash
} 

对于长字符串,&+ 运算符允许 Int 溢出并重新开始。

大图

我们已经查看了各个部分,但现在让我展示与 Hashable 协议相关的整个示例代码。 ScalarString 是问题中的自定义类型。当然,这对不同的人来说会有所不同。

// Include the Hashable keyword after the class/struct name
struct ScalarString: Hashable {

    private var scalarArray: [UInt32] = []

    // required var for the Hashable protocol
    var hashValue: Int {
        // DJB hash function
        return self.scalarArray.reduce(5381) {
            ([=15=] << 5) &+ [=15=] &+ Int()
        }
    }
}

// required function for the Equatable protocol, which Hashable inheirits from
func ==(left: ScalarString, right: ScalarString) -> Bool {
    return left.scalarArray == right.scalarArray
}

其他有帮助的读物

学分

非常感谢代码审查中的 Martin R。我的重写主要基于 his answer。如果觉得有帮助,请给他点个赞。

更新

Swift 现在是开源的,所以可以从 source code 中看到 hashValue 是如何为 String 实现的。它似乎比我在这里给出的答案更复杂,我没有花时间对其进行全面分析。随意自己做。