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

我正在制作一个类似于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 这篇文章有很多很棒的想法,但我没有看到它们在这种情况下会很好用。具体来说,

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

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

问题

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

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

更新

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

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

更新 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,感谢您在此处留下链接。我还在下面的回答中引用了你。

标签: ios arrays string swift hashtable


【解决方案1】:

一个建议 - 既然您正在建模 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 的过程,比我通过翼翼实现的更好......
【解决方案2】:

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

"\(scalarArray)".hashValue

scalarArray.description.hashValue

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

【讨论】:

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

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

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

更新

马丁 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 允许您将自定义类或结构用作字典键。为了实现这个协议,你需要

  1. 实现Equatable protocol(Hashable 继承自 Equatable)
  2. 返回一个计算出来的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
}

其他有用的阅读材料

学分

非常感谢代码审查中的 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。我不确定这是否对性能有任何影响,但我想指出两者在功能上并不等效。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-02-04
  • 1970-01-01
  • 2016-04-14
  • 1970-01-01
  • 1970-01-01
  • 2017-07-11
相关资源
最近更新 更多