簡體   English   中英

如何在Swift中為Int數組(自定義字符串結構)實現哈希協議

[英]How to implement the Hashable Protocol in Swift for an Int array (a custom string struct)

我正在制作一個類似於String的結構,只是它只處理Unicode UTF-32標量值。 因此,它是UInt32的數組。 (有關更多背景,請參閱此問題 。)

我想做的事

我希望能夠將自定義ScalarString結構用作字典中的鍵。 例如:

var suffixDictionary = [ScalarString: ScalarString]() // Unicode key, rendered glyph value

// populate dictionary
suffixDictionary[keyScalarString] = valueScalarString
// ...

// check if dictionary contains Unicode scalar string key
if let renderedSuffix = suffixDictionary[unicodeScalarString] {
    // do something with value
}

問題

為此, ScalarString需要實現Hashable Protocol 我以為我可以做這樣的事情:

struct ScalarString: Hashable {

    private var scalarArray: [UInt32] = []

    var hashValue : Int {
        get {
            return self.scalarArray.hashValue // error
        }
    }
}

func ==(left: ScalarString, right: ScalarString) -> Bool {
    return left.hashValue == right.hashValue
}

但是后來我發現Swift數組沒有hashValue

我讀了什么

《在Swift中實現哈希協議的策略》一書中有很多很棒的主意,但是我看不出有什么方法可以在這種情況下很好地工作。 特別,

  • 對象屬性 (數組是沒有hashValue
  • ID屬性 (不確定如何很好地實現)
  • 公式 (似乎任何32位整數字符串的公式都會占用大量處理器資源,並且有很多整數溢出)
  • ObjectIdentifier (我使用的是結構,而不是類)
  • 從NSObject繼承 (我使用的是結構,而不是類)

這是我閱讀的其他內容:

Swift字符串具有hashValue屬性,因此我知道可以做到這一點。

如何為自定義結構創建hashValue

更新

更新1:我想做一些不涉及轉換為String然后使用StringhashValue 創建我自己的結構的全部目的是為了避免進行很多String轉換。 String從某處獲取其hashValue 看來我可以使用相同的方法來獲得它。

更新2:我一直在研究其他上下文中字符串哈希碼算法的實現。 不過,我很難知道哪種方法最好,並用Swift來表達它們。

更新3

我寧願不要導入任何外部框架,除非這是推薦用於這些事情的方法。

我使用DJB哈希函數提交了可能的解決方案。

更新資料

馬丁·R 寫道

Swift 4.1開始 ,如果所有成員都符合Equatable / Hashable(SE0185),則編譯器可以自動合成EquatableHashable以實現類型一致性。 Swift 4.2開始 ,Swift標准庫(SE-0206)中內置了一個高質量的哈希組合器。

因此,不再需要定義自己的哈希函數,只需聲明一致性即可:

 struct ScalarString: Hashable, ... { private var scalarArray: [UInt32] = [] // ... } 

因此,下面的答案需要重寫(再次)。 在此之前,請從上面的鏈接中參考Martin R的答案。


舊答案:

將我的原始答案提交給代碼審查后,該答案已被完全重寫。

如何實現到哈希協議

哈希協議允許您將自定義類或結構用作字典鍵。 為了實施此協議,您需要

  1. 實現Equatable協議 (Hashable繼承自Equatable)
  2. 返回計算的hashValue

這些要點來自文檔中給出的公理:

x == y表示x.hashValue == y.hashValue

其中xy是某種類型的值。

實施平等協議

為了實現Equatable協議,您定義類型如何使用== (等效)運算符。 在您的示例中,等效性可以這樣確定:

func ==(left: ScalarString, right: ScalarString) -> Bool {
    return left.scalarArray == right.scalarArray
}

==函數是全局的,因此它超出了您的類或結構。

返回計算的hashValue

您的自定義類或結構還必須具有計算hashValue變量。 一個好的哈希算法將提供廣泛的哈希值。 但是,應注意,您不必保證哈希值都是唯一的。 當兩個不同的值具有相同的哈希值時,這稱為哈希沖突。 發生沖突時,這需要一些額外的工作(這就是為什么需要良好的分布)的原因,但是某些沖突是可以預期的。 據我了解, ==函數可以完成額外的工作。 更新看來==可以完成所有工作。

有多種計算哈希值的方法。 例如,您可以做一些簡單的事情,就像返回數組中的元素數一樣。

var hashValue: Int {
    return self.scalarArray.count
} 

每當兩個數組具有相同數量的元素但值不同時,就會產生哈希沖突。 NSArray顯然使用了這種方法。

DJB哈希函數

DJB哈希函數是與字符串一起使用的常見哈希函數。 這是我將要使用的那個,但是在這里請查看其他一些。

@MartinR提供的 Swift實現如下:

var hashValue: Int {
    return self.scalarArray.reduce(5381) {
        ($0 << 5) &+ $0 &+ Int($1)
    }
}

這是我原始實現的改進版本,但讓我也包括了較舊的擴展形式,對於不熟悉reduce人們可能更可讀。 我認為這是等效的:

var hashValue: Int {

    // DJB Hash Function
    var hash = 5381

    for(var i = 0; i < self.scalarArray.count; i++)
    {
        hash = ((hash << 5) &+ hash) &+ Int(self.scalarArray[i])
    }

    return hash
} 

&+運算符允許Int溢出並重新為長字符串重新開始。

大圖景

我們已經看過各個部分,但現在讓我展示與哈希協議相關的整個示例代碼。 ScalarString是問題中的自定義類型。 當然,這對於不同的人來說是不同的。

// Include the Hashable keyword after the class/struct name
struct ScalarString: Hashable {

    private var scalarArray: [UInt32] = []

    // required var for the Hashable protocol
    var hashValue: Int {
        // DJB hash function
        return self.scalarArray.reduce(5381) {
            ($0 << 5) &+ $0 &+ Int($1)
        }
    }
}

// required function for the Equatable protocol, which Hashable inheirits from
func ==(left: ScalarString, right: ScalarString) -> Bool {
    return left.scalarArray == right.scalarArray
}

其他有用的閱讀

學分

非常感謝Code Review中的MartinR。 我的改寫主要是基於他的回答 如果您覺得有幫助,請給他點贊。

更新資料

Swift現在是開源的,因此可以從源代碼中了解如何為String實現hashValue 它似乎比我在這里給出的答案更為復雜,並且我還沒有花時間對它進行全面分析。 自己動手做。

這不是一個很好的解決方案,但效果很好:

"\(scalarArray)".hashValue

要么

scalarArray.description.hashValue

只是使用文本表示作為哈希源

編輯(17年5月31日):請參閱接受的答案。 這個答案幾乎只是關於如何使用CommonCrypto框架的演示。

好的,我取得了成功,並通過使用CommonCrypto框架中的SHA-256哈希算法,使用Hashable協議擴展了所有數組。 你必須把

#import <CommonCrypto/CommonDigest.h>

到您的橋接頭中,以使其正常工作。 遺憾的是必須使用指針:

extension Array : Hashable, Equatable {
    public var hashValue : Int {
        var hash = [Int](count: Int(CC_SHA256_DIGEST_LENGTH) / sizeof(Int), repeatedValue: 0)
        withUnsafeBufferPointer { ptr in
            hash.withUnsafeMutableBufferPointer { (inout hPtr: UnsafeMutableBufferPointer<Int>) -> Void in
                CC_SHA256(UnsafePointer<Void>(ptr.baseAddress), CC_LONG(count * sizeof(Element)), UnsafeMutablePointer<UInt8>(hPtr.baseAddress))
            }
        }

        return hash[0]
    }
}

編輯(17年5月31日):即使SHA256幾乎沒有哈希沖突,也不要這樣做,但是通過哈希相等來定義相等是錯誤的想法

public func ==<T>(lhs: [T], rhs: [T]) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

這和CommonCrypto一樣好。 這是丑陋的,但速度快, 沒有多少 幾乎沒有哈希沖突肯定

編輯(15年7月15日):我剛剛進行了一些速度測試:

大小為n的隨機填充的Int數組平均運行1000次以上

n      -> time
1000   -> 0.000037 s
10000  -> 0.000379 s
100000 -> 0.003402 s

而使用字符串哈希方法:

n      -> time
1000   -> 0.001359 s
10000  -> 0.011036 s
100000 -> 0.122177 s

因此,SHA-256方式比字符串方式快33倍。 我並不是說使用字符串是一個很好的解決方案,但這是我們唯一可以與之比較的解決方案

一個建議-由於您正在建模一個String ,將[UInt32]數組轉換為String並使用StringhashValue是否hashValue 像這樣:

var hashValue : Int {
    get {
        return String(self.scalarArray.map { UnicodeScalar($0) }).hashValue
    }
}

這可以方便地使您也將自定義structString進行比較,盡管這是否是一個好主意取決於您要執行的操作...

還要注意,使用這種方法,如果ScalarString實例的String表示形式是規范等效的,則它們將具有相同的hashValue ,這可能是您想要的,也可能不是您想要的。

因此,我想如果您希望hashValue表示一個唯一的String ,那么我的方法會很好。 如果您希望hashValue表示UInt32值的唯一序列,則@Kametrixom的答案就是解決方法...

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM