20

私の質問はObject.GetHashCode() のデフォルトの実装と重複している可能性がありますが、その回答に対する受け入れられた回答が理解できなかったため、再度質問しています。

まず、前の質問に対する受け入れられた回答について 3 つの質問があります。この回答は、次のようなドキュメントを引用しています。

「ただし、このインデックスはガベージ コレクション中にオブジェクトが回収された後に再利用できるため、2 つの異なるオブジェクトに対して同じハッシュ コードを取得することができます。」

これは本当ですか?オブジェクトがガベージ コレクションされる (つまり、存在しなくなる) まで、オブジェクトのコードは再利用されないため、2 つのオブジェクトが同じハッシュ コードを持たないように思えます。

「また、同じ値を表す 2 つのオブジェクトは、それらがまったく同じオブジェクトである場合にのみ、同じハッシュ コードを持ちます。」

これは問題ですか?たとえば、あるデータを DOM ツリーの各ノード インスタンスに関連付けたいとします。これを行うには、データのディクショナリでキーとして使用できるように、「ノード」に ID またはハッシュ コードが必要です。それが「まったく同じオブジェクト」であるかどうか、つまり「値の等価性」ではなく「参照の等価性」であるかどうかを識別するハッシュコードではありませんか?

「この実装は、ハッシュには特に有用ではありません。したがって、派生クラスは GetHashCode をオーバーライドする必要があります」

これは本当ですか?ハッシュに適していない場合、何か良いことがあるとすれば、それはなぜオブジェクトのメソッドとして定義されているのでしょうか?


私の最後の(そしておそらく私にとって最も重要な)質問は、「参照の等価性」セマンティクスを持つ任意の型の GetHashCode() 実装を発明/オーバーライドする必要がある場合、合理的で適切な実装は次のとおりです。

class SomeType
{
  //create a new value for each instance
  static int s_allocated = 0;
  //value associated with this instance
  int m_allocated;
  //more instance data
  ... plus other data members ...
  //constructor
  SomeType()
  {
    allocated = ++s_allocated;
  }
  //override GetHashCode
  public override int GetHashCode()
  {
    return m_allocated;
  }
}

編集

参考までに、次のコードを使用してテストしました。

    class TestGetHash
    {
        //default implementation
        class First
        {
            int m_x;
        }
        //my implementation
        class Second
        {
            static int s_allocated = 0;
            int m_allocated;
            int m_x;
            public Second()
            {
                m_allocated = ++s_allocated;
            }
            public override int GetHashCode()
            {
                return m_allocated;
            }
        }
        //stupid worst-case implementation
        class Third
        {
            int m_x;
            public override int GetHashCode()
            {
                return 0;
            }
        }

        internal static void test()
        {
            testT<First>(100, 1000);
            testT<First>(1000, 100);
            testT<Second>(100, 1000);
            testT<Second>(1000, 100);
            testT<Third>(100, 100);
            testT<Third>(1000, 10);
        }

        static void testT<T>(int objects, int iterations)
            where T : new()
        {
            System.Diagnostics.Stopwatch stopWatch =
                System.Diagnostics.Stopwatch.StartNew();
            for (int i = 0; i < iterations; ++i)
            {
                Dictionary<T, object> dictionary = new Dictionary<T, object>();
                for (int j = 0; j < objects; ++j)
                {
                    T t = new T();
                    dictionary.Add(t, null);
                }
                for (int k = 0; k < 100; ++k)
                {
                    foreach (T t in dictionary.Keys)
                    {
                        object o = dictionary[t];
                    }
                }
            }
            stopWatch.Stop();
            string stopwatchMessage = string.Format(
                "Stopwatch: {0} type, {1} objects, {2} iterations, {3} msec",
                typeof(T).Name, objects, iterations,
                stopWatch.ElapsedMilliseconds);
            System.Console.WriteLine(stopwatchMessage);
        }
    }

私のマシンでは、結果/出力は次のとおりです。

First type, 100 objects, 1000 iterations, 2072 msec
First type, 1000 objects, 100 iterations, 2098 msec
Second type, 100 objects, 1000 iterations, 1300 msec
Second type, 1000 objects, 100 iterations, 1319 msec
Third type, 100 objects, 100 iterations, 1487 msec
Third type, 1000 objects, 10 iterations, 13754 msec

私の実装には、デフォルトの実装の半分の時間がかかります (ただし、私の型は m_allocated データ メンバーのサイズだけ大きくなっています)。

私の実装とデフォルトの実装はどちらも線形にスケーリングします。

比較すると、健全性チェックとして、愚かな実装は最初は悪く、スケーリングも悪くなります。

4

3 に答える 3

37

ハッシュ コードの実装に必要な最も重要なプロパティは次のとおりです。

2 つのオブジェクトを比較して等しい場合、それらは同一のハッシュ コードを持っている必要があります。

クラスのインスタンスが参照の等価性によって比較されるクラスがある場合、GetHashCode をオーバーライドする必要はありません。デフォルトの実装では、同じ参照である 2 つのオブジェクトが同じハッシュ コードを持つことが保証されます。(同じオブジェクトに対して同じメソッドを 2 回呼び出しているため、もちろん結果は同じです。)

参照の等価性とは異なる独自の等価性を実装するクラスを作成した場合は、等しいと比較される 2 つのオブジェクトが等しいハッシュ コードを持つように GetHashCode をオーバーライドする必要があります。

これで、毎回ゼロを返すだけでこれを行うことができます。それはお粗末なハッシュ関数になりますが、合法です。

優れたハッシュ関数のその他の特性は次のとおりです。

  • GetHashCode は決して例外をスローすべきではありません

  • 可変状態で等しいかどうかを比較し、可変状態でハッシュする可変オブジェクトは、危険なほどバグが発生しやすいです。オブジェクトをハッシュ テーブルに配置し、それを変更して、再び取得できないようにすることができます。ミュータブルな状態でハッシュしたり比較したりしないようにしてください。

  • GetHashCode は非常に高速である必要があります。優れたハッシュ アルゴリズムの目的は、ルックアップのパフォーマンスを向上させることです。ハッシュが遅い場合、ルックアップを高速にすることはできません。

  • 等しいと比較されないオブジェクトは、32 ビット整数の範囲全体に適切に分散された異なるハッシュ コードを持つ必要があります。

于 2009-07-16T20:21:55.373 に答える
2

質問:

これは本当ですか?オブジェクトがガベージ コレクションされる (つまり、存在しなくなる) まで、オブジェクトのコードは再利用されないため、2 つのオブジェクトが同じハッシュ コードを持たないように思えます。

デフォルトの GetHashCode 実装によって生成された場合、2 つのオブジェクトが同じハッシュ コードを共有することがあります。

  1. デフォルトの GetHashCode の結果は、オブジェクトの存続期間中に変更されるべきではなく、デフォルトの実装によってこれが保証されます。変更される可能性がある場合、Hashtable などの型はこの実装に対応できません。これは、デフォルトのハッシュ コードが一意のインスタンス識別子のハッシュ コードであることが予想されるためです (そのような識別子はありませんが :))。
  2. GetHashCode 値の範囲は整数 (2^32) の範囲です。

結論: 2^32 個の強く参照されたオブジェクトを (Win64 では簡単に) 割り当てて、制限に達するだけで十分です。

最後に、MSDN の object.GetHashCode 参照に明示的なステートメントがあります。GetHashCodeメソッドの既定の実装では、異なるオブジェクトに対して一意の戻り値が保証されません。さらに、.NET Framework は GetHashCode メソッドの既定の実装を保証しておらず、返される値は .NET Framework の異なるバージョン間で同じになります。したがって、このメソッドのデフォルトの実装は、ハッシュ目的で一意のオブジェクト識別子として使用しないでください。

于 2009-07-16T20:38:14.560 に答える
0

参照の同等性のみを必要とするクラスでは、実際には何も変更する必要はありません。

また、正式には、配布が不十分なため、これは適切な実装ではありません。ハッシュ関数は、ハッシュバケットの分散を改善し、間接的にハッシュテーブルを使用するコレクションのパフォーマンスを向上させるため、妥当な分散が必要です。私が言ったように、それは正式な答えであり、ハッシュ関数を設計する際のガイドラインの1つです。

于 2009-07-16T19:44:24.920 に答える