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)
}
}