2.8 Swift 3 為自定義類型實現(xiàn)Hashable Key

本質(zhì)上來說,Dictionary是一個哈希表,它所有的key都用各自的哈希值保存在一個數(shù)組里。因此,通過key在Dictionary中訪問value是一個O(1)操作。但這也對key的類型做出了一個要求,它必須可以計算哈希值。Swift標準庫中提供的絕大多數(shù)類型,例如:Int / Float / Double / String / Bool / Date ...等,都滿足這個要求,因此我們可以直接拿它們來定義Dictionary。

但如果我們有一個自定義類型Account,表示泊學(xué)的網(wǎng)站賬號,其中的alias / type / createdAt分別表示賬號的別名、類型和注冊日期:

 struct Account {
    var alias: String
    var type: Int
    var createdAt: Date
}

當我們把Account用作key的時候,Swift就會給我們提示下面的錯誤:Account沒有遵從Hashable protocol

let account11 = Account(alias: "11",
    type: 1, createdAt  : Date())
let data:[Account: Int] = [ account11: 1000 ]

Hashable requirement

Conform to Hashable protocol

如何讓自定義類型遵從Hashable protocol呢?第一件要做的事,就是告訴Swift,如何獲取一個類型的哈希值,這是通過一個叫hashValue的屬性完成的:

extension Account: Hashable {
    var hashValue: Int
}

如何計算Account.hashValue呢?有兩個最重要的考量,分別是:性能和哈希值在整數(shù)范圍的分布。因為每當我們要在Dictionary中查詢、添加、修改或刪除元素的時候,都要計算key的哈希值,如果這個計算過于消耗性能,那么計算哈希值的過程就有可能抵消掉通過key隨機訪問value帶來的O(1)性能提升。

當然,你也不能盲目追求性能而忽視哈希值的整數(shù)值分布。說一個最極端的例子,如果你讓所有情況計算得到的哈希值都是某個常數(shù):

extension Account: Hashable {
    // A BAD idea
    var hashValue: Int { return 1 }
}

這個哈希函數(shù)有O(1)的性能,但這樣,不同的Account對象就會有不同的哈希值,這叫做Collision。當然,Swift Dictionary可以處理哈希值碰撞的情況,但你要隨之付出的代價就是,通過哈希值讀取value將從一個O(1)變成一個O(n)算法。因此,讓哈希值在整數(shù)區(qū)間均勻分布也是設(shè)計哈希函數(shù)很重的考慮。

綜上所述,設(shè)計一個好的哈希函數(shù)并不是一個容易的事情。對于我們來說,可以假設(shè)Swift標準庫的類型提供的hashValue都滿足性能和分布的要求。因此,當我們設(shè)計復(fù)合類型的哈希值的時候,可以基于這些標準類型的哈希值進行一些“低功耗”運算,例如,對這些值進行異或運算,絕大多數(shù)的CPU都對這個操作提供了指令級支持:

extension Account: Hashable {
    var hashValue: Int {
        return alias.hashValue ^
                type.hashValue ^
                createdAt.hashValue
    }
}

解決了Account的哈希值之后,Swift會繼而提示我們:Account沒有遵從Equatable protocol。為什么還要遵從Equatable呢?這是因為哈希函數(shù)還有一個很重要的性質(zhì):兩個相等對象的哈希值必須是相同的。因此,我們必須要解決什么叫做兩個相等的對象,然后才有比較它們各自哈希值的事情。

Equatable只有一個約束,就是為自定義類型實現(xiàn)==操作符:

extension Account: Equatable {
    static func == (lhs: Account, rhs: Account) -> Bool {
        return lhs.alias == rhs.alias &&
            lhs.type == rhs.type &&
            lhs.createdAt == rhs.createdAt
    }
}

在Swift里,運算符必須要定義成static方法,它的兩個參數(shù)lhs / rhs則表示==兩邊的操作數(shù)。我們判斷Account相等的方式很簡單,只要它們每一個屬性相等,則兩個Account對象就是相等的。

當我們讓Account遵從了Equatable之后,Swift編譯器就不會再報錯了。此時,我們在一開始創(chuàng)建的data也可以正常工作了。

Bitwise rotation

我們上面例子中提到的把所有屬性進行XOR運算的方法,雖然簡單高效,但也有一個問題,就是比較容易造成碰撞。因為XOR運算是可交換的,也就是說a ^ b == b ^ a,因此,如果一個自定義類型中,有多個類型相同屬性的時候,就會增大哈希值發(fā)生碰撞的概率,因此,我們可以用下面的代碼,對其中的一些基礎(chǔ)屬性的哈希值進行按位旋轉(zhuǎn)后再進行XOR運算:

struct Account {
    let INT_BIT = (Int)(CHAR_BIT) * MemoryLayout<Int>.size

    func bitwiseRotate(value: Int, bits: Int) -> Int {
        return (((value) << bits) | ((value) >> (UINT_BIT - bits)))
    }
}

extension Account: Hashable {
    var hashValue: Int {
        return bitwiseRotate(value: alias.hashValue, bits: 10) ^
            type.hashValue ^
            createdAt.hashValue
    }
}

首先,我們在Account中添加了一個常量INT_BIT表示一個整數(shù)的位數(shù)。其次,定義了一個輔助方法bitwiseRotate(value:bits:),它用于先把value向左移動bits位,再向右移動(UINT_BIT - bits)位。

有了這個方法之后,我們就可以在計算哈希值的時候,對其中的屬性進行按位旋轉(zhuǎn)了。

警惕引用類型的Key

Dictionary.Key相關(guān)的最后一個內(nèi)容,是盡可能避免使用引用類型作為key,這通常會給你帶來不必要的麻煩。當一個引用類型作為key之后,當引用類型的對象在Dictionary之外被修改的時候,Key的內(nèi)容也會隨之修改。這樣你就再也無法獲得之前的哈希值,也就無法獲得對應(yīng)的value了。

What's next?

以上就是和如何用自定義類型作為Key相關(guān)的話題,至此,我們對Dictionary的討論就可以告一段落了。在下一節(jié)中,我們會看到Swift標準庫中的另外一個無序集合:Set。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容