Swift 中「等同性」、「比較」、「哈希」 概念理解

最近 Google 又搞了個大新聞:成功攻破了業界普遍使用的 SHA-1 哈希算法,加上看了 MrPeak 的 a 閒聊 Hash 算法 ,因此我就去仔細看了下 Swift 中的相關內容與概念。這篇文章算是對 Swift 中對象的「等同性」、「比較」、「哈希」概念的一個簡單介紹。算法

Equatable

Class 這種引用類型存在基於指針的默認的等同判斷,可是 Struct 所表明的值類型則沒有這個特性。並且有時候咱們也會對引用類型的等同判斷進行自定義實現,全部下面咱們經過 Struct 類型做爲示例來說解這些概念。咱們定義一個 Country 類型,其中包含國家名、首都、是否旅遊 三個屬性。api

struct Country {
    let name: String
    let capital: String
    var visited: Bool
}

接下來咱們新建一些實例變量並添加到數組中:數組

let canada = Country(name: "Canada", capital: "Ottawa", visited: true)
let australia = Country(name: "Australia", capital: "Canberra", visited: false)
...
let bucketList = [brazil,australia,canada,egypt,uk,france]

若是此時須要對 bucketList 變量進行檢查,判斷其中是否包含某個 Country 類型對象,那麼最直接的代碼實現多是:閉包

let object = canada
let containsObject = bucketList.contains { (country) -> Bool in
    return  country.name == object.name &&
            country.capital == object.capital &&
            country.visited == object.visited
}

固然上訴實現是有很大問題的。你不得不在每一處判斷中拷貝代碼,並且這種強耦合結構會後期對 Country 結構修改形成大麻煩。好在咱們可使用標準庫裏面的 Equatable 協議來進行 == 判斷的實現:函數

extension Country: Equatable {
    static func == (lhs: Country, rhs: Country) -> Bool {
        return  lhs.name == rhs.name &&
                lhs.capital == rhs.capital &&
                lhs.visited == rhs.visited
    }
}

改造後,不只對象 == 比較代碼寫起來簡單了,並且也讓代碼更易於維護。指針

bucketList.contains(canada)  // true

Comparable

若是此時須要對 bucketList 按升序進行排序的話又該如何應對呢?咱們可使用 Array 的排序閉包:code

bucketList.sorted(by: { $0.name < $1.name } )

爲了使上述代碼能正常工做,咱們須要對上面的 extension 進行修改,實現 Comparable 協議中的 < 方法。對象

extension Country: Comparable {

    static func == (lhs: Country, rhs: Country) -> Bool {
        return  lhs.name == rhs.name &&
                lhs.capital == rhs.capital &&
                lhs.visited == rhs.visited
    }

    static func < (lhs: Country, rhs: Country) -> Bool {
        return  lhs.name < rhs.name ||
                (lhs.name == rhs.name && lhs.capital < rhs.capital) ||
                (lhs.name == rhs.name && lhs.capital == rhs.capital && rhs.visited)
    }
}

固然,< 實現中屬性的比較順序徹底依據我的選擇。排序

Comparable 協議繼承自 Equatable 協議,其中還有 <=>>= 方法。繼承

Hashable

除了將自定義類型存入數組外,有時候咱們還須要將其存入 Dictionary、Set。甚至某些場景下還須要將其做爲鍵值對中的 Key,這就涉及到哈希函數以及哈希值的碰撞問題了。Swift 標準庫裏的類型,例如:String, Integer, Bool 都已經哈希函數而且能夠經過 hashValue 屬性直接得到哈希值:

let hello  = "hello"
let world = "world"
hello.hashValue                 // 4799432177974197528
"\(hello) \(world)".hashValue   // 3658945855109305670
"hello world".hashValue         // 3658945855109305670

對於咱們的自定義類型 Country 來講,咱們能夠取出每一個屬性的哈希值而後在進行異或操做。

extension Country: Hashable {
    var hashValue: Int {
        return name.hashValue ^ capital.hashValue ^ visited.hashValue
    }
}

// 這樣 Country 類型對象就能夠做爲 Key了。
let counts = [uk: 1000, canada: 2000]

上面 Country 實現了 Hashable 協議,而且可以在應用於 Dictionary、Set 中,可是這裏還有一些問題須要注意。

  • 咱們知道相同的對象的哈希值是同樣的,而哈希值相同則並不表示對象相同。這意味着哈希碰撞一定存在,可是咱們能夠採用一些方法來減小碰撞域。

  • 對於 Bool 類型的對象來講,它的哈希值只多是0或1,因此其不能單獨用於生成哈希值。

下面咱們經過一段代碼來直觀感覺下哈希碰撞(此處只考慮哈希碰撞,不要糾結於變量含義):

let london = Country(name: "London", capital: "London", visited: false)
let paris = Country(name: "Paris", capital: "Paris", visited: false)
london.hashValue  // 0
paris.hashValue   // 0

這段代碼中由於每一個對象自身的 namecapital 屬性相同,而 londonparisvisited 屬性也相同,最後加上抑或操做的特色,兩個對象無可避免的發生了哈希碰撞。由於抑或操做中 A ^ B = B ^ A 的特性,下面這張碰撞狀況也常發生:

let canada = Country(name: "Canada", capital: "Ottawa", visited: false)
let ottawa = Country(name: "Ottawa", capital: "Canada", visited: false)
canada.hashValue  // 3695199242423112
ottawa.hashValue  // 3695199242423112

這就尷尬了。不過仔細查看代碼,咱們會發現上訴衝突的緣由之一就是 namecapital 屬性採用了一樣的哈希函數。若是咱們對其中某一個屬性的哈希進行改造那麼必定程度上能減小碰撞域。固然哈希函數並非隨手寫一個就行的,咱們能夠參照 [哈希函數] [1] 一文實現其中的 djb2sdbm

extension String {
    var djb2hash: Int {
    let unicodeScalars = self.unicodeScalars.map { $0.value }
        return unicodeScalars.reduce(5381) {
            ($0 << 5) &+ $0 &+ Int($1)
        }
    }

    var sdbmhash: Int {
        let unicodeScalars = self.unicodeScalars.map { $0.value }
            return unicodeScalars.reduce(0) {
                Int($1) &+ ($0 << 6) &+ ($0 << 16) - $0
        }
    }
}

並修改 Country 中的哈希實現:

extension Country: Hashable {
    var hashValue: Int {
        return name.djb2hash ^ capital.hashValue ^ visited.hashValue
    }
}

改進後上訴衝突得以解決:

let london = Country(name: "London", capital: "London", visited: false)
let paris = Country(name: "Paris", capital: "Paris", visited: false)
london.hashValue  // 4792642925948815646
paris.hashValue   // 4799464424543103873

let canada = Country(name: "Canada", capital: "Ottawa", visited: false)
let ottawa = Country(name: "Ottawa", capital: "Canada", visited: false)
canada.hashValue  // 4792300300145562762
ottawa.hashValue  // 4795361053083927978

總結

本文簡單的介紹了 Swift 中「等同性」、「比較」、「哈希」的概念,並對一些常見哈希衝突進行了分析。固然了,這樣一篇文章遠遠沒法全方位覆蓋這些知識點,尤爲是哈希相關的內容,這些都留給你們本身去探索吧。

相關文章
相關標籤/搜索