Swift 4.2 新特性详解 Hashable 和 Hasher

Hashable 的 Conditional Conformance

使用 DictionarySet 的时候要求用作 Key 的类型实现 Hashable 协议。由于大多数内置类型天生是 Hashable,因此大多数情况下,无需手动实现。但是对于一个自定义的类型,需要由我们来实现 Hashable。然而实现var hashValue: Int 并非如它的接口那么显而易见。其中的原因我们在 Swift 4.1 新特性 (3) 合成 Equatable 和 Hashable 中详细的讨论过了,其中也讲到编译器在一定条件下会帮助合成 Hashable 中的函数。例如:

struct Person: Hashable { 
  var age: Int 
  var name: String 
} 

上述代码在 Swift 4.1 和 Swift 4.2 中都可以编译过,由于 Hashable is a Equatable,所以编译器实际上自动合成了 == 以及 hashValue 两个函数。但是下一个相似的例子却在 Swift 4.1 中编译不过,在 Swift 4.2 中可以编译过。

struct Person: Hashable { 
  var age: Int 
  var pets: [String] 
} 

这是为什么呢?其实这是由于 [String] 在 Swift 4.1 中不是 Hashable,所以编译器无法合成;而在 Swift 4.2 中由于标准库中添加了一组 Hashable 的 Conditional Conformance 扩展,所以可以合成。其中包含:

extension Array : Hashable where Element : Hashable

其含义是:当 Array 的元素是 Hashable 时,这个 Array 也是 Hashable:由于String 本身是 Hashable,所以[String] 在 Swift 4.2 中是 Hashable,编译器的自动合成得以继续。

有关 Conditional Conformance,我们在另一篇文章中已经进行了详细的讨论 Swift 4.2 新特性详解 Conditional Conformance 的更新,它属于泛型特性,不是标准库的特权,我们完全自己也可以定义。在 Swift 4.2 中,如果有重复的定义,编译器会给出警告。

简化 Hashable 的实现

即便编译器合成 Hashable的情况在 Swift 4.2 中得到了进一步的改进,我们在很多情况下也不得不自己实现 Hashable

  • class 类型声明 Hashable
  • extension 中声明 Hashable
  • 有数据成员需要排除出 hashValue 计算时
  • 自己能够提供更好的 hashValue 实现时

首先,我们看一下,一个好的 hashValue 实现在 Swift 4.1 中是怎么样的:

// Swift 4.1
struct Person: Hashable {
  var age: Int
  var name: String

  var hashValue: Int {
     return age.hashValue ^ name.hashValue &* 16777619
  }
}

这段代码要求开发人员对于如何计算一个哈希值非常专业:首先 ^ 是异或,&* 是防止乘法溢出 crash 的运算符,16777619 显然也不是一个随便选择的数字。所以简化 Hashable 第一个目的,是要简化 Hash 算法给程序员带来的心智负担。因此,在 Swift 4.2 中,实现同样的功能简化成为:

// Swift 4.2
struct Person: Hashable {
  var age: Int
  var name: String

func hash(into hasher: inout Hasher) {
  hasher.combine(age)
  hasher.combine(name)
  }
}

在这段代码中,转而实现的是 Hashable 中定义的新方法 func hash(into hasher: inout Hasher),在这个方法的实现中,我们 99 % 的情况只要调用 hasher.combine,传入需要纳入 Hash 计算的 Hashable 数据成员即可。对于字节流,Hasher 提供另一个combine方法。我们来看一下 Hasher 的定义:

// Swift 4.2
public struct Hasher {
 
public mutating func combine<H>(_ value: H) where H : Hashable
public mutating func combine(bytes: UnsafeRawBufferPointer)
public __consuming func finalize() -> Int
}

而谁负责传入这个 Hasher 呢?其实是编译器自动生成的另一个 Hashable 的老方法 hashValue ,如下:

// Swift 4.2 supplied by the compiler
var hashValue: Int {
  var hasher = Hasher()
  self.hash(into: &hasher)
  return hasher.finalize()
}

最后调用 finalize 一次生成最后的计算结果。可以看到新的 Hashable 设计不仅简化了用户的实现代码,还将计算 Hash 的职责抽离,使得将来在不改变用户代码的情况下,也能在标准库中优化计算 Hash 的代码。

Hashable 的向后兼容

由于 Hashable 作为协议加了一个新的方法, Swift 4.2 之前的代码还能编译过吗?答案是可以,编译器自动生成新的方法的实现如下:

// Supplied by the compiler:
func hash(into hasher: inout Hasher) {
  hasher.combine(self.hashValue)
}

因此,在 Swift 4.2 下,实现任意一个 Hashable 的函数都可以通过编译,但我们推荐实现新的 hash(into:) 函数。

Hashable 的性能

首先,我们需要了解我们自己的代码可能带来的潜在性能问题。

struct Point: Hashable {
  var x: Int
  var y: Int
}

struct Line: Hashable {
  var begin: Point
  var end: Point

  func hash(into hasher: inout Hasher) {
    hasher.combine(begin.hashValue) // potential performance issue
    hasher.combine(end) // correct
  }
}

在这个例子中,我们不应当『提前』计算出 beginhashValue,尽管这从结果上是可行的。而是应当像 end 那样仅仅像Hasher提出计算需求。那么combine 究竟做了什么呢?来看源码:

@inlinable
@inline(__always)
public mutating func combine<H: Hashable>(_ value: H) {
  value.hash(into: &self)
}

简单来看,combine仅仅是一个语法糖,实质上形成的是 Hashable.hash(into:)的层层调用。为了消除这个语法糖带来的函数调用性能影响,标准库将它的接口定义和实现统统作为模块的一部分暴露出来了,允许用户代码内联,这就是@inlinable的作用。而且只有实现稳定到与接口一样的程度,才应该这样声明。与@inlinable配合的是@usableFromInline,它同样作为模块ABI的一部分(但不作为API),@inlinable的函数可以调用@usableFromInline函数。这是Swift 4.2 的一个不常用的新特性,也是 Hashable 性能相关的另一方面。

Hashable 多次执行中的随机行为

最后我们讨论一下 1.hashValue 的值到底是什么?在 Xcode 9 中,他永远是固定的;然而在 Xcode 10 中它在每次运行的时候数字都不一样。

-9043285239196511288
-3192328192178018481
2941366561895793247

这是因为新的版本的默认行为是在程序每次执行的时候,加入不同的随机Seed,因此在多次运行过程中的结果是不同的,一次程序运行时候的多次1.hashValue的调用结果是保持相同的。这个默认行为可以通过将环境变量 SWIFT_DETERMINISTIC_HASHING 设置成 1 变回原先的方式,但是我们不推荐,因为 Hash 每次执行加入随机性是为了防止哈希碰撞的攻击,这对于特别是服务端上 的 Swift 程序是有很重要价值的。

小结

  • 讨论了标准库中新加入的 Hashable Conditional Conformance,以及它对于自动合成 Hashable 的意义。
  • 默认情况下,在 Swift 4.2 中实现 Hashable 的新方法、不实现老方法。或者在恰当的情况下依赖编译器的自动合成。
  • 编译器的自动合成行为 保证了 Swift 4.2 前的 Hashable 的实现代码的向后兼容。
  • Hashable 性能相关的问题:实现 Hashable 不要提前计算出局部 hashValue 以及@inlinable消除函数调用性能消耗。
  • Hashable 多次执行中的随机性是为了解决潜在的哈希碰撞攻击。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容