Swift 的 Hashable protocol

Swift 裡有一個特別的 Hashable protocol,我們在很多地方都可以看到它的蹤跡。

比方 Set 要求它的成員型別一定要遵從 Hashable,因此遵從 Hashable 的字串可以存入 Set,沒有遵從 Hashable 的 CGPoint 資料無法。

struct Set<Element> where Element : Hashable

讓自訂型別遵從 Hashable protocol

當型別遵從 Hashable protocol 時,它可以產生一個稱為 hash value 的特別數字。這類的資料很適合存在 Set 或 Dictionary 裡,因為 Set 或 Dictionary 可以利用此數字區分資料跟快速找到資料。

Swift 裡很多基本的型別都遵從 Hashable,比方 String,Int,Double 等。不過我們也可以讓自訂的型別遵從 Hashable。

當 struct 的 property 都遵從 Hashable 時,我們只要讓它遵從 Hashable,不用定義 Hashable 的 function hash(into:),因為 Swift 可以幫我們產生。以下例子 Swift 產生的 function hash(into:) 將利用 name & height 產生 hash value。

struct Hero: Hashable {
let name: String
let height: Double
}

let hero1 = Hero(name: "Peter Pan", height: 180)
let hero2 = Hero(name: "Peter Park", height: 173)
let set = Set(arrayLiteral: hero1, hero2)

而 enum 則更厲害,當它沒有 associated value 時將自動遵從 Hashable protocol。當它有 associated value,而且 associated value 的型別遵從 Hashable protocol 時,跟剛剛的 struct 一樣,我們只要讓它遵從 Hashable,不用定義 Hashable 的 function hash(into:),因為 Swift 可以幫我們產生。

看來讓自訂型別遵從 Hashable protocol 滿容易的,我們甚至不用定義它的 function hash(into:)。不過還是有幾種例外的 case。

  • 當 struct 的 property 不遵從 Hashable
  • 當型別是 class。
  • 我們想自己定義 function hash(into:)。
  • 搭配 associated value 的 enum,而且 associated value 的型別沒有遵從 Hashable protocol。

因此接下來讓我們看個自訂 hash(into:) 的例子。

自訂 function hash(into:)

以下例子裡,我們自訂 function hash(into:)。hasher.combine(name) & hasher.combine(gender) 表示我們將利用 name & gender 計算 hash value,而 height 則對 hash value 無任何影響。

struct Hero: Hashable {
let name: String
let height: Double
let gender: String
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(gender)
}
}

let hero1 = Hero(name: "Peter Pan", height: 180, gender: "Male")
let hero2 = Hero(name: "Peter Pan", height: 173, gender: "Male")
let hero3 = Hero(name: "Peter Pan", height: 160, gender: "Female")

因此同樣是男彼得潘的 hero1 & hero2 有一樣的 hash value,但女彼得潘 hero3 則有不同的 hash value。

若是我們沒有自訂 hash(into:),Hero 產生 hash value 時將使用到每個 property,因此它將同時考慮 name,height & gender,最後 hero1,hero2 & hero3 將產生不同的 hash value。

利用 name & gender 判斷英雄是否重覆

Set 可以保證集合裡的成員不會有重覆的。以剛剛的型別 Hero 為例,我們想在 set 裡裝不重覆的英雄,只要 name & gender 不一樣就當成不同的英雄。因此 Hero 型別必須加入以下程式

  • 遵從 protocol Hashable
  • function hash 裡利用 name & gender 計算 hash value。
  • function == 利用 name & gender 判斷是否相等。
struct Hero: Hashable {
var name: String
var height: Double
var gender: String

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

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.name == rhs.name && lhs.gender == rhs.gender
}
}

let hero1 = Hero(name: "Peter Pan", height: 180, gender: "Male")
let hero2 = Hero(name: "Peter Pan", height: 173, gender: "Male")
let hero3 = Hero(name: "Peter Pan", height: 160, gender: "Female")
let set = Set(arrayLiteral: hero1, hero2, hero3)

定義 function ==

剛剛提到某些情況我們要自己定義 hash(into:)。然而當 property 不遵從 Hashable 時,我們還要自己定義 function ==,例如以下例子。

struct Pet {
let name: String
}

struct Hero: Hashable {
let name: String
let pet: Pet
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(pet.name)
}
}

以上程式將產生錯誤 does not conform to protocol Equatable。

為什麼會有這個錯誤呢 ? 這和 protocol Hashable 的定義有關。protocol Hashable 本身又遵從 Equatable,所以當我們的自訂型別遵從 Hashable 時,它同時也要遵從 Equatable。

protocol Hashable : Equatable

當 struct 的 property 都遵從 Equatable 時,我們只要讓它遵從 Equatable,Swift 即可幫我們定義 Equatable 的 function ==。因此以下的 Hero 型別遵從 protocol Hashable & Equatable,而且我們不用自己定義 protocol 的 function。

struct Hero: Hashable {
let name: String
let height: Double
}

然而剛剛出問題的例子裡,Hero 的 property pet 型別為 Pet,Pet 並不遵從 Hashable & Equatable,因此我們得自己定義 function == & hash(into:)。

struct Hero: Hashable {
let name: String
let pet: Pet

static func == (lhs: Hero, rhs: Hero) -> Bool {
return lhs.name == rhs.name && lhs.pet.name == rhs.pet.name
}

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

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com