web-dev-qa-db-ja.com

Int配列(カスタム文字列構造体)のSwift)でハッシュ可能プロトコルを実装する方法

UnicodeのUTF-32スカラー値のみを処理することを除いて、Stringのように動作する構造を作成しています。したがって、それは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
}

問題

そのためには、ScalarStringHashable 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プロパティ(これをどのようにうまく実装できるかわからない)
  • Formula(32ビット整数の文字列の式はプロセッサーに負荷がかかり、整数オーバーフローが多いようです)
  • ObjectIdentifier(クラスではなく構造体を使用しています)
  • NSObjectからの継承(クラスではなく構造体を使用しています)

ここに私が読んだ他のいくつかのものがあります:

質問

Swift Stringsには hashValue プロパティがあるため、実行できることがわかっています。

カスタム構造のhashValueを作成するにはどうすればよいですか?

更新情報

更新1:Stringに変換してからStringhashValue。独自の構造を作成するための私の全ポイントは、多くのString変換を行わないようにすることでした。 Stringは、どこかからhashValueを取得します。私は同じ方法を使用してそれを得ることができるようです。

更新2:他のコンテキストからの文字列ハッシュコードアルゴリズムの実装を検討してきました。ただし、どちらが最適かを知り、それらをSwiftで表現するのは少し困難です。

Update 3

これらのことを行うのに推奨される方法でない限り、外部フレームワークをインポートしないことを希望します。

DJBハッシュ関数を使用して可能な解決策を提出しました。

40
Suragch

更新

マーティンR 書き込み

Swift 4.1の時点で、すべてのメンバーがEquatable /に準拠している場合、コンパイラはEquatableおよびHashableを型適合のために自動的に合成できます。ハッシュ可能(SE0185)。そしてSwift 4.2の時点で、高品質のハッシュコンバイナーがSwift標準ライブラリ(SE -0206)。

したがって、独自のハッシュ関数を定義する必要はもうありません。適合性を宣言するだけで十分です。

struct ScalarString: Hashable, ... {

    private var scalarArray: [UInt32] = []

    // ... }

したがって、以下の答えは書き直す必要があります(まだ)。それが起こるまで、上記のリンクからのMartin Rの答えを参照してください。


古い答え:

コードレビューに対する元の回答 を送信した後、この回答は完全に書き直されました。

Hashableプロトコルに実装する方法

ハッシュ可能プロトコル を使用すると、カスタムクラスまたは構造体を辞書キーとして使用できます。このプロトコルを実装するには、次のことが必要です。

  1. Equatableプロトコル の実装(ハッシュ可能はEquatableから継承)
  2. 計算されたhashValueを返します

これらのポイントは、ドキュメントに記載されている公理に基づいています。

x == yx.hashValue == y.hashValueを意味します

ここで、xおよびyは、あるタイプの値です。

Equatableプロトコルを実装する

Equatableプロトコルを実装するには、タイプで==(等価)演算子を使用する方法を定義します。あなたの例では、等価性は次のように決定できます。

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

==関数はグローバルなので、クラスまたは構造体の外部に移動します。

計算されたhashValueを返します

カスタムクラスまたは構造体には、計算されたhashValue変数も必要です。適切なハッシュアルゴリズムは、広範囲のハッシュ値を提供します。ただし、ハッシュ値がすべて一意であることを保証する必要はないことに注意してください。 2つの異なる値に同一のハッシュ値がある場合、これはハッシュ衝突と呼ばれます。衝突がある場合は追加の作業が必要です(これが適切な分布が望ましい理由です)が、いくつかの衝突が予想されます。私が理解しているように、==関数はその特別な仕事をします。 (Update==は、all作業を行う可能性があるようです。

ハッシュ値を計算する方法はいくつかあります。たとえば、配列内の要素の数を返すなどの簡単なことを実行できます。

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

これにより、2つの配列の要素数が同じで値が異なるたびにハッシュ衝突が発生します。 NSArrayは明らかにこのアプローチを使用しています。

DJBハッシュ関数

文字列で機能する一般的なハッシュ関数は、DJBハッシュ関数です。これは私が使用するものですが、他のいくつかをチェックしてください here

A Swift implementation @ MartinRにより提供 が続きます:

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がオーバーフローし、長い文字列に対して最初からやり直すことができます。

大局

これらの部分を見てきましたが、Hashableプロトコルに関連するサンプルコード全体を示します。 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のMartin Rに感謝します。私の書き換えは、主に 彼の答え に基づいています。これが役に立つと思ったら、彼に賛成票をください。

更新

Swiftは現在オープンソースであるため、 ソースコード からhashValueに対してStringがどのように実装されているかを確認できます。ここで示した答えよりも複雑なように見えますが、十分に時間をかけて分析していません。自分でそうしてください。

46
Suragch

これは非常にエレガントなソリューションではありませんが、うまく機能します。

"\(scalarArray)".hashValue

または

scalarArray.description.hashValue

ハッシュソースとしてテキスト表現を使用するだけです

4
Kametrixom

編集(17年5月31日):受け入れられた回答を参照してください。この答えはほとんどCommonCrypto Frameworkの使用方法のデモンストレーションです

さて、先に進み、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で得られるものと同じです。 ugいですが、高速で あまりないハッシュ衝突はほとんどありません

編集(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倍高速です。文字列を使用することが非常に良い解決策であると言っているわけではありませんが、それを今と比較できる唯一のものです

4
Kametrixom

1つの提案-Stringをモデリングしているので、[UInt32]配列をStringに変換し、StringhashValueを使用することはできますか?このような:

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

これにより、カスタムstructStringsと比較することもできますが、それが良いアイデアかどうかは、何をしようとしているかによって異なります...

また、このアプローチを使用すると、ScalarStringの表現が標準的に同等である場合、hashValueのインスタンスは同じStringを持つことに注意してください。

したがって、hashValueが一意のStringを表すようにしたい場合、私のアプローチは良いと思います。 hashValueUInt32値の一意のシーケンスを表すようにする場合、@ Kametrixomの答えは...

3
Aaron Rasmussen