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

Posted

技术标签:

【中文标题】如何在 Swift 中为 Int 数组(自定义字符串结构)实现 Hashable 协议【英文标题】:How to implement the Hashable Protocol in Swift for an Int array (a custom string struct) 【发布时间】:2015-07-15 18:24:25 【问题描述】:

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

我想做什么

我希望能够使用我的自定义 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 这篇文章有很多很棒的想法,但我没有看到它们在这种情况下会很好用。具体来说,

对象属性(数组没有hashValueID 属性(不确定如何很好地实现) 公式(似乎任何用于 32 位整数字符串的公式都会占用大量处理器并且有大量整数溢出) ObjectIdentifier(我使用的是结构,而不是类) 从 NSObject 继承(我使用的是结构,而不是类)

以下是我阅读的其他一些内容:

Implementing Swift's Hashable Protocol Swift Comparison Protocols Perfect hash function Membership of custom objects in Swift Arrays and Dictionaries How to implement Hashable for your custom class Writing a good Hashable implementation in Swift

问题

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

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

更新

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

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

Java hashCode algorithm C algorithms hash function for string(C 中的 SO 问题和答案) Hashing tutorial(弗吉尼亚理工大学算法可视化研究组) General Purpose Hash Function Algorithms

更新 3

我不希望导入任何外部框架,除非这是推荐的方式。

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

【问题讨论】:

旁白:UTF-32 不能处理一个代码单元中的所有 unicode 字符。有代理对、表情符号颜色和奇怪的东西,比如需要不止一个的标志。 Unicode 使用 21 位来存储它的所有代码点,因此 UTF-32 足以处理上面planes 中的标志、表情符号和其他东西。但是,UTF-16 编码需要代理对来引用平面 0 之上的任何 Unicode 字符。也就是说,UInt32 无法 处理 Swift Characters(扩展字形簇),它可以由多个 Unicode 标量值。我自己制作ScalarString 的原因是为了避免Character 的一些歧义。 从 Swift 4.2 开始,不再需要定义自己的哈希组合器,请参阅我对 codereview.stackexchange.com/a/111573/35991 的更新。 @MartinR,感谢您在此处留下链接。我还在下面的回答中引用了你。 【参考方案1】:

更新

马丁 Rwrites:

Swift 4.1 开始,编译器可以合成 EquatableHashable 如果所有成员都符合,则自动符合类型 Equatable/Hashable (SE0185)。从 Swift 4.2 开始,一个高质量的哈希 combiner 内置在 Swift 标准库 (SE-0206) 中。

因此不再需要定义自己的散列 函数,声明一致性就足够了:

struct ScalarString: Hashable, ... 

    private var scalarArray: [UInt32] = []

    // ... 

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


旧答案:

提交我的original answer to code review后,此答案已完全重写。

如何实现Hashable协议

Hashable protocol 允许您将自定义类或结构用作字典键。为了实现这个协议,你需要

    实现Equatable protocol(Hashable 继承自 Equatable) 返回一个计算出来的hashValue

这些点遵循文档中给出的公理:

x == y 暗示x.hashValue == y.hashValue

其中xy 是某种类型的值。

实现 Equatable 协议

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

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

== 函数是全局的,因此它位于您的类或结构之外。

返回一个计算出来的hashValue

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

有多种方法可以计算哈希值。例如,您可以执行一些简单的操作,例如返回数组中的元素数。

var hashValue: Int 
    return self.scalarArray.count
 

每当两个数组具有相同数量的元素但不同的值时,这都会产生哈希冲突。 NSArray 显然使用了这种方法。

DJB 哈希函数

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

一个 Swift 实现 provided by @MartinR 如下:

var hashValue: Int 
    return self.scalarArray.reduce(5381) 
        ($0 << 5) &+ $0 &+ Int($1)
    

这是我原始实现的改进版本,但让我也包括旧的扩展形式,对于不熟悉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
 

&amp;+ 运算符允许 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) 
            ($0 << 5) &+ $0 &+ Int($1)
        
    


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

其他有用的阅读材料

Which hashing algorithm is best for uniqueness and speed? Overflow Operators Why are 5381 and 33 so important in the djb2 algorithm? How are hash collisions handled?

学分

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

更新

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

【讨论】:

我已经删除了我的评论,我忽略了一件重要的事情,这是用于处理哈希冲突的isEqual。所以这可能是一个很好的解决方案,并且比 SHA-256 快得多。唯一缺少的是确保两个对象的类是相同的。由于它不是 Array 的扩展,而只是类中的一个方法,因此对于同构数组来说这是一个可行的解决方案。 请参阅 Mattt Thompson 的 Equality & Identity。 “关于实现自定义散列函数的最常见误解之一来自于肯定结果,认为散列值必须是不同的。......实际上,对关键属性的散列值进行简单的异或运算在 99% 的情况下就足够了。” 感谢您使用reduce的想法! 仅供参考 - 我不认为您的方法示例的扩展形式与 reduce 方法等效。在 reduce 方法中,$0 和 $1 的值将随着值的减少而改变每次迭代(因此最初将是 5381,但下一个将是第一次减少的值),而在 for 循环示例中,变量 'hash' 保持不变在 5381。我不确定这是否对性能有任何影响,但我想指出两者在功能上并不等效。【参考方案2】:

编辑(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 日):我刚刚做了一些速度测试:

随机填充的 Int 大小为 n 的数组平均运行 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 倍。我并不是说使用字符串是一个很好的解决方案,但它是我们现在唯一可以与之比较的解决方案

【讨论】:

您是否考虑过这可能会如何影响其他类型的数组? @zaph 没有。这种实现对于任何类型的每个数组都是完全安全的。 只是为了我的教育,为什么这对其他类型的数组没有影响。 我刚刚阅读了另一个答案,说应该避免对哈希表使用加密哈希。 ***.com/a/7666668/3681880 这种情况不适用吗? 我刚刚做了一些速度测试,请查看编辑。 @Suragch也许SHA-256对于散列标准来说很慢,但它仍然非常快,并且肯定不会导致很多(甚至任何)散列冲突。我猜这取决于你在寻找什么【参考方案3】:

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

"\(scalarArray)".hashValue

scalarArray.description.hashValue

仅使用文本表示作为哈希源

【讨论】:

这两种方式都将数组转换为String,然后得到StringhashValue。你知道String 是如何得到它的hashValue 的吗?好像如果我知道的话,我可以直接做到这一点,而不必通过String 无论如何都是有趣的解决方案。【参考方案4】:

一个建议 - 既然您正在建模 String,那么将您的 [UInt32] 数组转换为 String 并使用 StringhashValue 是否可行?像这样:

var hashValue : Int 
    get 
        return String(self.scalarArray.map  UnicodeScalar($0) ).hashValue
    

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

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

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

【讨论】:

这是一个有趣的想法,而且很有效。不过,我有点犹豫要不要使用它,因为制作这个自定义UInt32 数组的整个想法是避免转换为String。你知道String 是如何得到它的hashValue 的吗? 不过,我相信 Swift 优化了计算字符串 hashValue 的过程,比我通过翼翼实现的更好......

以上是关于如何在 Swift 中为 Int 数组(自定义字符串结构)实现 Hashable 协议的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Python 中用“”替换我的自定义字符?

自定义字符类

Linux内核开发——自定义字符设备

Linux内核开发——自定义字符设备

java:打印菱形图案(传参打印的自定义字符和行数)

R语言str_flatten函数通过自定义字符连接(concatenate)字符串向量中的字符串