web-dev-qa-db-ja.com

順序を考慮したFooオブジェクトのリストのGetHashCode()オーバーライド

_EnumerableObject : IEnumerable<Foo>_

_List<Foo>_をラップします

EnumerableObject a.SequenceEquals( EnumerableObject b)の場合、それらは等しいです。

したがって、GetHashCodeを実装する必要があります。問題は、リストの各要素のXORを実行すると、順序に関係なく、すべての要素が同じであるすべてのリストに対して同じハッシュコードが返されることです。これは機能的には問題ありませんが、多くの衝突が発生し、検索が遅くなります。

順序に依存するオブジェクトのリストに適した高速なGetHashCodeメソッドとは何ですか?

30
Ben B.

私は通常のハッシュコードを組み合わせるのと同じ方法で、加算と乗算を行います。

public override int GetHashCode()
{
    unchecked
    {
        int hash = 19;
        foreach (var foo in foos)
        {
            hash = hash * 31 + foo.GetHashCode();
        }
        return hash;
    }
}

(これは、説明がハッシュテーブルでキーに使用された後は、リストに何も追加しないでください。これは、ハッシュが変更されるためです。これは、nullエントリがないことも前提としています。それを考慮する必要があります。)

60
Jon Skeet

まず、ハッシュコードが必要かどうかを再確認してください。これらのリストをハッシュマップ構造(ディクショナリ、ハッシュセットなど)に入れますか?そうでない場合は、忘れてください。

ここで、EnumerableObjectがすでにEquals(object)をオーバーライドしている(つまり、うまくいけば、何らかの理由でIEquatable<EnumerableObject>も実装している)と仮定すると、これは実際に必要です。速度とビット配分のバランスをとりたい。

良い出発点は、次のようなmult + addまたはshift + xorです。

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    foreach(var item in this)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

(これは、シーケンスの同等性の比較にitem.Equals()を使用していることを前提としています。IEqualityComparerのequalsを使用している場合は、ハッシュコードを呼び出す必要があります)。

そこから最適化できます。

Nullアイテムが許可されていない場合は、nullチェックを削除します(注意してください。これにより、nullが見つかった場合にコードがスローされます)。

非常に大きなリストが一般的である場合は、衝突の数が多くならないようにしながら、調査する数を減らす必要があります。以下の異なる実装を比較してください。

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int max = Math.Min(Count, 16);
    for(int i = 0, i != max; ++i)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int min = Math.Max(-1, Count - 16);
    for(int i = Count -1, i != min; --i)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int step = Count / 16 + 1;
    for(int i = 0, i < Count; i += step)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

これらはそれぞれ、検査されるアイテムの総数を制限します。これにより、実行が高速化されますが、品質の低いハッシュが発生するリスクがあります。どちらが適切かは、開始と終了が同じコレクションの可能性が高いかどうかによって異なります。

上記の16を変更すると、バランスが調整されます。小さいほど高速ですが、高いほどハッシュの品質が高くなり、ハッシュの衝突のリスクが低くなります。

編集:そして、あなたは私の SpookyHash v。2の実装 を使用することができます:

public override int GetHashCode()
{
  var hasher = new SpookyHash();//use methods with seeds if you need to prevent HashDos
  foreach(var item in this)
    hasher.Update(item.GetHashCode());//or relevant feeds of item, etc.
  return hasher.Final().GetHashCode();
}

これにより、mult + addまたはshift + xorよりもはるかに優れた分散が作成されますが、特に高速でもあります(特に、64ビットプロセスではアルゴリズムが最適化されているため、32ビットでもうまく機能します)。

13
Jon Hanna

.GetHashCode()メソッドは通常、オブジェクト参照(ポインタアドレス)に基づいてハッシュを返すだけです。これは、列挙可能なリスト内のすべてのアイテムのハッシュコードの計算に非常に時間がかかる可能性があるためです。既存の動作を上書きするのではなく、拡張メソッドを使用して、ハッシュコードを決定論的に決定する必要がある場合にのみ拡張メソッドを使用することを好みます。

public static class EnumerableExtensions
{
    public static int GetSequenceHashCode<TItem>(this IEnumerable<TItem> list)
    {
        if (list == null) return 0;
        const int seedValue = 0x2D2816FE;
        const int primeNumber = 397;
        return list.Aggregate(seedValue, (current, item) => (current * primeNumber) + (Equals(item, default(TItem)) ? 0 : item.GetHashCode()));
    }
}
4
MovGP0