Swift 4.1 新特性 (3) 合成 Equatable 和 Hashable

編譯器合成 (synthesize) 是十分重要的功能,它把開發者從簡單重複的勞動中解放出來。在Swift 4.0 中,我們知道 Codable 可以合成相關函數,於是 Codable 的實現者在大部分情況下不需要實現相關函數。 到了 Swift 4.1,Equatable 和 Hashable 也支持了合成。觸發合成的一個重要的必要非充分條件是:包含的存儲屬性或相關值全都是 Codable / Equatable / Hashable,編譯器纔有可能推導出這個類型的相關函數實現。

我們先複習一下相關概念:

  1. 如果對象相等,則這兩個對象的 hash 值一定相等。

  2. 如果兩個對象 hash 值相等,這兩個對象不一定相等。

  3. Swift 中 Hashable 一定是 Equatable,因爲前者繼承了後者。

  4. 修改 == 函數的時候需要考慮是否同步修改 hashValue,反之亦然。

  5. Dictionary 和 Set 的中的 Key 類型都要求是 Hashable

1. 合成 Equatable

我們以前得手寫如下代碼。缺點:1. 實現很冗長無聊。2. 增刪改一個屬性還得記得改這個函數。

struct Person: Equatable {
  static func == (lhs: Person, rhs: Person) -> Bool {
    return 
    lhs.firstName == rhs.firstName &&
    lhs.lastName == rhs.lastName &&
    lhs.birthDate == rhs.birthDate &&
...
  }
}

現在舒服了,聲明下 : Equatable即可,編譯器幫你合成 == 函數的實現。

struct Person: Equatable { ... }

這樣是否意味着可以無腦申明 Equatable 了呢?並不是這樣。在某些屬性不參與相等比較時,必須自己實現,讓編譯器不要合成。舉個例子,假如 Person 有一個屬性叫 createdTime,記錄了它被創建的時間,如果我們不希望這個屬性參與相等比較,就需要自己實現 == 函數。

我們回顧一下 enum 在 Swift 4.1 版本前的情況,以下三種情況哪些可以編譯通過呢?

// eg1
enum SSS {
    case a
    case b
}
SSS.a == SSS.b

// eg2
enum KKK : String {
    case a
    case b
}
KKK.a == KKK.b

// eg3
enum Token {
    case string(String)
    case number(Int)
    case lparen
    case rparen
}
Token.string("123") == Token.string("456")

答案是:例子1、2能編譯過,例子 3編譯不過。SSS 是最簡單的 enum,KKK是帶有 rawType 的 enum,這兩個在 Swift 4.1 之前就自動是 Equatable 和 Hashable,即便不顯式聲明
例子3是帶 associated value 的 enum,在 Swift 4.1 之前需要實現 Equatable,不僅要聲明 :Equatable,還得自己寫 == 方法,你還記得怎麼寫嗎?,不妨不要看下面的答案,練習一下 Pattern Matching。

  static func == (lhs: Token, rhs: Token) -> Bool {
    switch (lhs, rhs) {
    case (.string(let lhsString), .string(let rhsString)):
      return lhsString == rhsString
    case (.number(let lhsNumber), .number(let rhsNumber)):
      return lhsNumber == rhsNumber
    case (.lparen, .lparen), (.rparen, .rparen):
      return true
    default:
      return false
    }
  }

在 Swift 4.1 中,例子 3 我們僅僅需要聲明 :Equatable 就能讓編譯器幫我們合成==方法,太方便了。

2. 合成 Hashable

試想一下,上面那個帶 associated value 的 enum,如何實現它的 hashValue方法呢?有可能你已經有了答案,但這裏同樣可以聲明 :Hashable讓編譯器合成。

接下來我們來回顧下什麼是 hash 函數。這個函數目的是:將原來對象的域映射到 Int 的值域。筆者認爲 hash 函數設計的難點有兩個:

  1. 如何將一個輸入的域(對象的可取值範圍),映射到一個一般來說更小的域(Int),同時又儘可能防止不同的對象得到同一個 Int,(這種情況叫“衝突”,衝突不可避免,但大規模衝突會大幅降低對象檢索效率,最簡單的hash函數實現是 return 1,但卻是最糟糕的hash函數)
  2. hash 函數效率非常重要,它可能會被非常頻繁地調用。除了 hash 函數算法本身,我們可以設計緩存策略,特別是在不可變情況比較多的 struct 和 enum 的時候。

基於以上兩點:編譯器合成的 hash 函數能保證高質量,但很有可能不是最優的。因爲編譯器無法得到一些只有你知道的信息:比如屬性的實際值域:var age: Int(不可能是負數;如果是 Person 結構,取值範圍在0-200等),又比如屬性之間的關係特性,而往往你可以利用這些信息設計出更優的 hash 函數。

合成 Hashable 跟 Equatable 一樣,聲明 :Hashable 之後,可以自己實現,來壓制編譯器的合成行爲。

Hashable 和 Equatable 還有一些編譯器不合成的情況需要特別指出:

  1. class 不合成,原因是繼承情況下比較複雜,合成出來也不一定是你要的。
  2. extension 聲明實現 Hashable 或 Equatable 時也不合成。

其他:標準庫 Index 類型支持 Hashable

除了同樣是 Swift 4.1 的新特性並且同樣跟 Hashable 相關,跟本文主題沒太多關係,這裏增強的是:標準庫將 Index 類型都實現了 Hashable。

我們知道 Swift 有個特性叫強類型的 Key Path。如果 Key Path 中用下標表達式的話,下標類型必須是 Hashable 的,Int 原本就是,而String.Index原來不是,所以下面例子中第二段的代碼只在 Swift 4.1 中是合法的。

let numbers = [10, 20, 30, 40, 50]
let firstValue = \[Int].[0]
print(numbers[keyPath: firstValue])     // 10

let string = "Helloooo!"
let firstChar = \String.[string.startIndex] // valid in Swift 4.1 or later

小結

在本文中,我們探討了:

  • 合成 Hashable和 Equatable 的價值。
  • 合成的侷限性,需要自己書寫函數的情況和注意點。
  • 編譯器不合成的情況。
  • 標準庫 Index 類型支持 Hashable,增強Key Path表達式能力。

Swift 4.1 新特性系列文章

Swift 4.1 新特性 (1) Conditional Conformance
Swift 4.1 新特性 (2) Sequence.compactMap
Swift 4.1 新特性 (3) 合成 Equatable 和 Hashable
Swift 4.1 新特性 (4) Codable的改進

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章