iOS-swiftiOS技术资料Swift

Swift 4.2 新特性详解 Hashable 和 Hashe

2018-11-28  本文已影响84人  面试官小健

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

首先,我们看一下,一个好的 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 程序是有很重要价值的。

小结

上一篇下一篇

猜你喜欢

热点阅读