Hashable / Hasher
作者:Mattt,原文連結,原文日期:2018-08-13 譯者:ofollow,noindex">Damonwong ;校對:Lision,Yousanflics;定稿:Forelax
當你在蘋果商店預約天才吧服務後,相關工作人員會幫你登記並且安排特定的服務時間,在被帶到座位上之後,工作人員會記錄你的身份資訊並新增到服務隊列當中。
根據一份來自某位前零售店員工的報告表示,對於顧客的描述有著嚴格的指導方針。他們的外貌特徵如:年齡、性別、種族、身高都沒有被使用 —— 甚至連頭髮的顏色都沒有被使用。而是通過顧客的著裝來描述,例如“黑色的高領毛衣,牛仔褲和眼鏡”
這種描述顧客的方式和程式設計中的雜湊函式有很多共同之處。同許多優秀的雜湊函式一樣,它是連續和易計算的,可用於快速找到你正在尋找的內容(或者人)。這比使用佇列要好多了,我想你一定會同意的。
這周我們的主題是Hashable
和相關的新型別Hasher
。它們共同組成了Swift
最受喜愛的兩個集合類Dictionary
和Set
的基礎功能。
假設你有一個可以比較相等性的物件列表。要在這個列表
中找到一個特定的物件,你需要遍歷這個列表
的元素,直到找到匹配項為止。隨著你向列表
中新增更多的元素時,需要找到其中任何一個元素所需的平均時間是線性級的(O(n)
)。
如果將這些物件儲存在一個集合中,理論上可以在常量級時間(O(1)
)內找到它們中的任何一個 - 也就是說,在一個包含 10 個元素的集合
中查詢或在一個包含 10000*
個元素的集合
中查詢所需的時間是一樣的。這是怎麼回事呢?因為集合
不是按順序儲存物件的,而是將物件內容計算的雜湊值
作為索引儲存。當在集合
中查詢物件時,可以使用相同的雜湊函式計算新的雜湊值然後查詢物件儲存位置。
* 如果兩個不同的物件具有相同的雜湊值時,會產生雜湊衝突。當發生雜湊衝突時,它們將儲存在該地址對應的列表中。物件之間發生衝突的概率越高,雜湊集合的效能就會更加線性增長。
Hashable
在Swift
中,Array
為列表提供了標準的接⼝,Set
為集合提供了標準的接⼝。如果要將物件儲存到Set
中,就要遵循Hashable
協議及其擴充套件協議Equatable
。Swift
的標準對映介面Dictionary
對它的關聯型別Key
也需要遵循Hashable
協議及其擴充套件協議。
在Swift
之前的版本中,為了讓自定義型別能支援Set
或Dictionary
儲存需要寫⼤量的樣板程式碼。
以下面的Color
型別為例,Color
使⽤了 8 位整型值來表示紅,綠,藍色值:
struct Color { let red: UInt8 let green: UInt8 let blue: UInt8 } 複製程式碼
要符合Equatable
的要求,你需要提供一個 == 操作符的實現。要符合Hashable
的要求,你需要提供⼀個名為hashValue
的計算屬性:
// Swift < 4.1 extension Color: Equatable { static func ==(lhs: Color, rhs: Color) -> Bool { return lhs.red == rhs.red && lhs.green == rhs.green && lhs.blue == rhs.blue } } extension Color: Hashable { var hashValue: Int { return self.red.hashValue ^ self.green.hashValue ^ self.blue.hashValue } } 複製程式碼
對於大多數開發者⽽⾔,實現Hashable
只是為了能儘快讓要做的事情步入正軌,因此他們會對所有的儲存屬性使⽤異或操作,並在某一天呼叫它。
然⽽這種實現的一個缺陷是高雜湊衝突率。由於異或操作滿⾜交換率,像⻘色和⻩色這樣不同的顏色也會發⽣雜湊衝突:
// Swift < 4.2 let cyan = Color(red: 0x00, green: 0xFF, blue: 0xFF) let yellow = Color(red: 0xFF, green: 0xFF, blue: 0x00) cyan.hashValue == yellow.hashValue // true, collision 複製程式碼
大多數時候這樣做不會出問題;現代計算機已經足夠強大以至於你很難意識到效能的衰減,除⾮你的實現細節存在⼤量問題。
但這並不是說這些細節⽆關緊要 —— 它們往往極其重要。稍後會詳細介紹。
自動合成 Hashable 實現
從Swift 4.1
開始,如果某個型別在宣告時遵循了Equatable
和Hashable
協議並且它的成員變數同時也滿足了這些協議,編譯器會為其自動合成Equatable
和Hashable
的實現。
除了大大的提高了開發人員的開發效率以外,還可以大幅減少程式碼的數量。比如,我們之前Color
的例子 —— 現在是最開始程式碼量的 1/3 :
// Swift >= 4.1 struct Color: Hashable { let red: UInt8 let green: UInt8 let blue: UInt8 } 複製程式碼
儘管對語言進行了明顯的改進,但還是有一些實現細節有著無法忽視的問題。
在Swift Evolution
提案
SE-0185: 合成Equatable
和Hashable
的實現
中,Tony Allevato
給雜湊函式提供了這個註釋:
雜湊函式的選擇應該作為實現細節,而不是設計中的固定部分;因此,使用者不應該依賴於編譯器自動生成的 Hashable 函式的具體特徵。最可能的實現是在每個成員的雜湊值上呼叫標準庫中的_mixInt
函式,然後將他們異或組合(^),如同目前Collection
型別的雜湊方式一樣。
幸運的是,Swift 不需要多久就能解決這個問題。我們將在下一個版本得到答案:
Hasher
Swift 4.2通過引入Hasher
型別並採用新的通用雜湊函式進一步優化Hashable
在Swift Evolution 提案SE-0206: Hashable 增強 中:
使用一個好的雜湊函式時,簡單的查詢,插入,刪除操作都只需要常量級時間即可完成。然而,如果沒有為當前資料選擇一個合適的雜湊函式,這些操作的預期時間就會和雜湊表中儲存的資料數量成正比。
正如Karoy Lorentey
和Vincent Esche
所指出的那樣,Set
和Dictionary
等基於雜湊的集合主要特點是它們能夠在常量級時間內查詢值。如果雜湊函式不能產生一個均勻的值分佈,這些集合實際上就變成了連結串列。
Swift 4.2中的雜湊函式是基於偽隨機函式族SipHash 實現的,特別是SipHash-1-3 and SipHash-2-4 ,每個訊息塊有 1 或 2 輪雜湊,分別有 3 或 4 輪最後確定。
現在,如果你要自定義型別實現Hashable
的方式,可以重寫hash(into:)
方法而不是hashValue
。hash(into:)
通過傳遞了一個Hasher
引用物件,然後通過這個物件呼叫combine(_:)
來新增型別的必要狀態資訊。
// Swift >= 4.2 struct Color: Hashable { let red: UInt8 let green: UInt8 let blue: UInt8 // Synthesized by compiler func hash(into hasher: inout Hasher) { hasher.combine(self.red) hasher.combine(self.green) hasher.combine(self.blue) } // Default implementation from protocol extension var hashValue: Int { var hasher = Hasher() self.hash(into: &hasher) return hasher.finalize() } } 複製程式碼
通過抽象隔離底層的位操作細節,開發人員可以利用Swift 內建的雜湊函式,這樣可以避免再現我們原有的基於異或實現的衝突:
// Swift >= 4.2 let cyan = Color(red: 0x00, green: 0xFF, blue: 0xFF) let yellow = Color(red: 0xFF, green: 0xFF, blue: 0x00) cyan.hashValue == yellow.hashValue // false, no collision 複製程式碼
自定義雜湊函式
預設情況下,Swift 使用通用的雜湊函式將位元組序列縮減為一個整數。
但是,你可以使用你專案中自定義的雜湊函式來改進這個縮減的問題。比如,如果你正在編寫一個程式來玩國際象棋或者棋盤遊戲,你可以使用Zobrist hashing 來快速的儲存遊戲的狀態。
避免雜湊氾濫(Hash-Flooding)
選擇像SipHash 這樣的加密演算法有助於防止雜湊氾濫的DoS 攻擊,這種攻擊會嘗試生成雜湊衝突,並試圖強制實施雜湊資料結構最壞的情況,最終導致程式慢下來。這在 2010 年初引發了一系列的網路問題 。
為了使事情變的更加安全,Hasher
會在每次啟動應用程式時生成一個隨機種子值,使得雜湊值更難以預測。
你不應該依賴特定的雜湊值或者在執行中儲存它們。在極少數情況下,你確定要這麼做的話,可以設定識別符號 SWIFT_DETERMINISTIC_HASHING 以禁用隨機雜湊種子。
程式設計類比的挑戰在於它們通過邊界情況規範反社會行為。
當我們能夠考慮到攻擊者所有可能利用來達到某種險惡目的的情況時,這時能體現出我們優秀工程師的品質 —— 比如雜湊氾濫的 DoS 攻擊。在現實生活中,這麼做我們需要冒著失敗的風險去應用這些 AFK(Away From Keyboard)知識。
也就是說...親愛的讀者,在下次您訪問當地的蘋果零售店時,我不鼓勵你和你的朋友協調服飾,以試圖在天才吧中製造混亂和不和諧。
請不要這麼做。
相反的,希望你有下面的收穫:
當你在天才吧等候的時候,遠離那些和自己穿同色襯衫的人。這會讓每個人做事都變得容易得多。
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問swift.gg。