Object.GetHashCode()のデフォルト実装


162

のデフォルトの実装はどのように機能しますGetHashCode()か?また、構造、クラス、配列などを効率的かつ十分に処理しますか?

私はどのような場合に自分でパックする必要があるのか​​、そしてデフォルトの実装をうまく利用するために安全に信頼できるケースを決定しようとしています。可能であれば、車輪を再発明したくありません。


私が記事に残したコメントをご覧
Paul Westcott


34
脇:あなたがすることができ得る(場合でも、デフォルトのハッシュコードをGetHashCode()使用することによってオーバーライドされている)System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
マルクGravell

@MarcGravellこれに貢献してくれてありがとう、私はまさにこの答えを探していました。
Andrew Savinykh 2013

@MarcGravellしかし、他の方法でこれをどのように行うのですか?
トマーシュZato -復活モニカ

回答:


86
namespace System {
    public class Object {
        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern int InternalGetHashCode(object obj);

        public virtual int GetHashCode() {
            return InternalGetHashCode(this);
        }
    }
}

InternalGetHashCodeは、次のようなCLRのObjectNative :: GetHashCode関数にマップされます。

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {  
    CONTRACTL  
    {  
        THROWS;  
        DISABLED(GC_NOTRIGGER);  
        INJECT_FAULT(FCThrow(kOutOfMemoryException););  
        MODE_COOPERATIVE;  
        SO_TOLERANT;  
    }  
    CONTRACTL_END;  

    VALIDATEOBJECTREF(obj);  

    DWORD idx = 0;  

    if (obj == 0)  
        return 0;  

    OBJECTREF objRef(obj);  

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);        // Set up a frame  

    idx = GetHashCodeEx(OBJECTREFToObject(objRef));  

    HELPER_METHOD_FRAME_END();  

    return idx;  
}  
FCIMPLEND

GetHashCodeExの完全な実装はかなり大きいため、C ++ソースコードにリンクするだけの方が簡単です


5
そのドキュメントの引用は、非常に初期のバージョンからのものである必要があります。現在のMSDNの記事では、このように書かれていません。おそらくそれがかなり間違っているためです。
ハンスパッサント2010

4
彼らは言い回しを変更しました、はい、しかしそれでも基本的に同じことを言います:「その結果、このメソッドのデフォルト実装はハッシュ目的でユニークなオブジェクト識別子として使用されてはなりません。」
David Brown

7
なぜドキュメントはハッシュに実装が特に有用ではないと主張しているのですか?オブジェクトがそれ自体と同等であり、特定のオブジェクトインスタンスに対して常に同じ値を返し、異なるインスタンスに対して異なる値を返すハッシュコードメソッドがある場合、何が問題なのでしょうか。
スーパーキャット2013年

3
@ ta.speot.is:特定のインスタンスがすでに辞書に追加されているかどうかを確認する必要がある場合は、参照の等価性は完璧です。文字列を使用すると、通常、同じ文字シーケンスを含む文字列が既に追加されているかどうかに関心があります。これがをstring上書きする理由GetHashCodeです。一方、さまざまなコントロールがPaintイベントを処理する回数をカウントしたいとします。あなたは使用することができますDictionary<Object, int[]>(すべてのint[]保存されたアイテムはちょうど1つのアイテムを保持します)。
スーパーキャット2013

6
@ It'sNotALie。次に、コピーを作成してくれたArchive.orgに感謝;-)
RobIII、2013年

88

クラスの場合、デフォルトは基本的に参照等価であり、通常はそれで問題ありません。構造体を作成する場合、(特にボクシングを回避するために)等価性をオーバーライドする方が一般的ですが、とにかく構造体を作成することは非常にまれです!

平等をオーバーライドするとき、あなたは常に一致していなければならないEquals()GetHashCode()(つまり、二つの値のため、場合はEquals()リターンが真、彼らがしなければならない同じハッシュコードを返しますが、その逆はされていない必要) -そしてまた、提供するのが一般的である==/ !=オペレータを、そして多くの場合にIEquatable<T>あまりにも実装します。

ハッシュコードの生成には、因数分解された合計を使用するのが一般的です。これにより、たとえば基本的な2フィールドハッシュの場合、ペアの値の衝突が回避されます。

unchecked // disable overflow, for the unlikely possibility that you
{         // are compiling with overflow-checking enabled
    int hash = 27;
    hash = (13 * hash) + field1.GetHashCode();
    hash = (13 * hash) + field2.GetHashCode();
    return hash;
}

これには次の利点があります。

  • {1,2}のハッシュは{2,1}のハッシュと同じではありません
  • {1,1}のハッシュは{2,2}のハッシュと同じではありません

etc-これは、加重されていない合計、またはxor(^)などを使用する場合に一般的です。


因数分解アルゴリズムの利点に関する優れた点。これまで気づかなかったこと!
ループホール2013

(上記のように)因数分解された合計がオーバーフロー例外を時々引き起こしませんか?
sinelaw 2013年

4
@sinelawはい、実行する必要がありますunchecked。幸い、これuncheckedはC#のデフォルトですが、明示的にすることをお勧めします。編集
マークグラベル

7

ドキュメントGetHashCodeのための方法オブジェクトが言う「このメソッドのデフォルトの実装は、ハッシュの目的のためにユニークなオブジェクト識別子として使用することはできません。」ValueTypeの1つは、「派生型のGetHashCodeメソッドを呼び出す場合、戻り値はハッシュテーブルのキーとしての使用には適さない可能性が高い」と述べています。

基本的なデータのようなタイプのbyteshortintlongcharstring良いGetHashCodeメソッドを実装します。Pointたとえば、他のいくつかのクラスと構造は、GetHashCode特定のニーズに適している場合とそうでない場合があるメソッドを実装しています。それを試してみて、十分かどうかを確認する必要があります。

各クラスまたは構造のドキュメントで、デフォルトの実装をオーバーライドするかどうかを確認できます。それを上書きしない場合は、独自の実装を使用する必要があります。GetHashCodeメソッドを使用する必要がある場所で自分で作成したクラスまたは構造体については、適切なメンバーを使用してハッシュコードを計算する独自の実装を作成する必要があります。


2
独自の実装を定期的に追加する必要があるとは思いません。簡単に言えば、クラスの大部分(特に)が同等であるかどうかはテストされません。同等の場合は、組み込みの参照の同等性で問題ありません。構造体を作成する(すでにまれな)機会では、それはより一般的であり、真実です。
Marc Gravell

@Marc Gravel:もちろんそれは私が言うつもりではありません。最後の段落を調整します。:)
グッファ

基本的なデータ型は、少なくとも私の場合、適切なGetHashCodeメソッドを実装していません。(123).GetHashCode()戻り123:たとえば、GetHashCodeメソッドは整数の番号自体返す
fdermishin

5
@ user502144それで何が問題になっていますか?これは、計算が簡単で完全な一意識別子であり、等式の誤検出はありません...
Richard Rast

@Richard Rast:Hashtableでキーを使用すると、キーが正しく配布されない場合を除いて、問題ありません。この答えを見てください:stackoverflow.com/a/1388329/502144
fdermishin

5

オーバーライドする必要がある理由GetHashCodeEqualsカスタム構造体の理由、およびデフォルトの実装が「ハッシュテーブルのキーとしての使用に適していない可能性がある」理由を説明する答えが見つからなかったため、このブログへのリンクを残しますpostは、発生した問題の実際の例で理由を説明しています。

私は投稿全体を読むことをお勧めしますが、ここに要約があります(強調と説明が追加されています)。

構造体のデフォルトのハッシュが遅く、あまり良くない理由:

CLRの設計方法、System.ValueTypeまたはで定義されたメンバーへのすべての呼び出しはSystem.Enumボクシング割り当てを引き起こす可能性があります[...]

ハッシュ関数の実装者はジレンマに直面しています:ハッシュ関数の良い分布を作るか、それを速くするために。場合によっては、両方を実現することも可能ですが、これをで一般的行うことは困難ValueType.GetHashCodeです。

構造体の正規ハッシュ関数は、すべてのフィールドのハッシュコードを「組み合わせ」ます。ただし、ValueTypeメソッド内のフィールドのハッシュコードを取得する唯一の方法は、リフレクション使用することです。したがって、CLRの作成者はディストリビューションで速度をトレードすることを決定し、デフォルトGetHashCodeバージョンは最初のnull以外のフィールドのハッシュコードを返し、それをタイプIDで「変更」します[...]これが適切でない場合を除き、これは妥当な動作です。たとえば、運が悪ければ、構造体の最初のフィールドの値がほとんどのインスタンスで同じあれば、ハッシュ関数は常に同じ結果を提供します。また、ご想像のとおり、これらのインスタンスがハッシュセットまたはハッシュテーブルに格納されている場合は、パフォーマンスが大幅に低下します。

[...] リフレクションベースの実装は遅いです。非常に遅い。

[...]両方ValueType.EqualsValueType.GetHashCode特別な最適化を持っています。型に「ポインター」がなく、適切にパックされている場合[...]より最適なバージョンが使用されGetHashCodeます。インスタンスと4バイトのXORブロックを反復処理し、Equalsメソッドを使用して2つのインスタンスを比較しますmemcmp。[...]しかし、最適化は非常にトリッキーです。第一に、最適化がいつ有効になるかを知るのは困難です[...]第二に、メモリ比較は必ずしも正しい結果を与えるとは限りません。ここで簡単な例である:[...] -0.0+0.0同じであるが、異なるバイナリ表現を有します。

投稿で説明されている実際の問題:

private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
    // Empty almost all the time
    public string OptionalDescription { get; }
    public string Path { get; }
    public int Position { get; }
}

デフォルトの等値実装を持つカスタム構造体を含むタプルを使用しました。そして残念ながら、構造体にはオプションの最初のフィールドがあり、ほとんどの場合[空の文字列]と同じでした。セット内の要素の数が大幅に増加して実際のパフォーマンスの問題が発生し、数万のアイテムを含むコレクションを初期化するのに数分かかるまで、パフォーマンスは問題ありませんでした。

したがって、「構造体の場合は、どのような場合に自分でパックし、どのような場合にデフォルトの実装に安全に依存できるか」という質問に答えるには、オーバーライドEqualsGetHashCode、カスタム構造体をハッシュテーブルのキーまたはDictionary。ボクシングを回避するために、この場合
も実装することをお勧めしますIEquatable<T>

他の答えが言ったように、あなたがクラスを書いている場合、参照の等価性を使用したデフォルトのハッシュは通常は問題ないので、オーバーライドする必要がない限り、この場合は気にしませんEquals(それに応じてオーバーライドする必要がありますGetHashCode)。


1

一般的に、Equalsをオーバーライドする場合は、GetHashCodeをオーバーライドする必要があります。これは、クラス/構造体の同等性を比較するために両方が使用されるためです。

EqualsはFoo A、Bをチェックするときに使用されます。

(A == B)の場合

ポインターが一致する可能性が低いことがわかっているので、内部メンバーを比較できます。

Equals(obj o)
{
    if (o == null) return false;
    MyType Foo = o as MyType;
    if (Foo == null) return false;
    if (Foo.Prop1 != this.Prop1) return false;

    return Foo.Prop2 == this.Prop2;
}

GetHashCodeは通常、ハッシュテーブルで使用されます。クラスによって生成されるハッシュコードは、クラスの状態を与えるために常に同じである必要があります。

私は通常、

GetHashCode()
{
    int HashCode = this.GetType().ToString().GetHashCode();
    HashCode ^= this.Prop1.GetHashCode();
    etc.

    return HashCode;
}

ハッシュコードはオブジェクトの有効期間ごとに1回だけ計算する必要があると言う人もいますが、私はそれに同意しません(そして私はおそらく間違っています)。

オブジェクトによって提供されるデフォルトの実装を使用して、クラスの1つへの同じ参照がない限り、それらは互いに等しくありません。EqualsとGetHashCodeをオーバーライドすることで、オブジェクト参照ではなく、内部値に基づいて同等性を報告できます。


2
^ =アプローチは、ハッシュを生成するための特に良好な方法ではない-それは、共通の/予測可能な衝突のロットにつながる傾向がある-例えば、もしPROP1 = PROP2 = 3
マルクGravell

値が同じであれば、オブジェクトが等しいため、衝突の問題は発生しません。13 * Hash + NewHashは面白そうです。
Bennett Dill、

2
ベン:Obj1と{PROP1 = 12、PROP2 = 12}とOBJ2 {PROP1 = 13、PROP2 = 13}のためにそれを試してみてください
トマシュカフカ

0

POCOだけを扱っている場合は、このユーティリティを使用して生活をいくらか簡略化できます。

var hash = HashCodeUtil.GetHashCode(
           poco.Field1,
           poco.Field2,
           ...,
           poco.FieldN);

...

public static class HashCodeUtil
{
    public static int GetHashCode(params object[] objects)
    {
        int hash = 13;

        foreach (var obj in objects)
        {
            hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
        }

        return hash;
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.