web-dev-qa-db-ja.com

C#のGetHashCodeガイドライン

Essential C#3.0および.NET 3.5の本で次のことを読みました。

GetHashCode()の戻り値は、オブジェクトのデータが変更された場合でも、特定のオブジェクトの寿命にわたって一定(同じ値)である必要があります。多くの場合、これを強制するにはメソッドの戻り値をキャッシュする必要があります。

これは有効なガイドラインですか?

.NETでいくつかの組み込み型を試しましたが、このように動作しませんでした。

131
Joan Venge

答えはほとんど、有効なガイドラインですが、おそらく有効なルールではありません。また、ストーリー全体を伝えるわけでもありません。

可変型の場合、2つの等しいオブジェクトが同じハッシュコードを返さなければならず、ハッシュコードはオブジェクトの存続期間中有効でなければならないため、可変型の場合、可変データに基づいてハッシュコードを作成できません。ハッシュコードが変更された場合、正しいハッシュビンに存在しなくなるため、ハッシュコレクションでオブジェクトが失われます。

たとえば、オブジェクトAは1のハッシュを返します。したがって、ハッシュテーブルのビン1に入ります。次に、オブジェクトAを変更して、2のハッシュを返すようにします。ハッシュテーブルがそれを探しているときに、ビン2を探して見つからない-オブジェクトはビン1で孤立しているため、ハッシュコードは変わらない オブジェクトの寿命の間、そしてGetHashCode実装を書くのが苦痛である理由の1つにすぎません。

更新
Eric Lippertがブログを投稿しました これは、GetHashCodeに関する優れた情報を提供します。

追加アップデート
上記のいくつかの変更を行いました。

  1. ガイドラインとルールを区別しました。
  2. 私は「オブジェクトの存続期間中」を打破しました。

ガイドラインは単なるルールであり、ルールではありません。実際には、GetHashCodeは、オブジェクトがハッシュテーブルに格納されている場合など、オブジェクトがガイドラインに従うことが期待される場合にのみ、これらのガイドラインに従う必要があります。ハッシュテーブル(またはGetHashCodeのルールに依存する他のオブジェクト)でオブジェクトを使用するつもりがない場合、実装はガイドラインに従う必要はありません。

「オブジェクトの存続期間中」と表示された場合は、「オブジェクトがハッシュテーブルと連携する必要がある間」などを読む必要があります。ほとんどの場合と同様に、GetHashCodeはルールを破るタイミングを知ることです。

89
Jeff Yates

それは長い時間でしたが、それでも、なぜかどうやってかについての説明を含めて、この質問に正しい答えを与える必要があると思います。これまでの最善の答えは、MSDNを徹底的に引用したものです。独自のルールを作ろうとしないでください。MSの人たちは、彼らが何をしていたかを知っていました。

しかし、まず最初に:質問で引用されているガイドラインは間違っています。

理由-2つあります

最初の理由:ハッシュコードが何らかの方法で計算される場合、オブジェクト自体が変更されても、等号契約を破るよりも、オブジェクトの存続期間中に変更されない。

要確認:「2つのオブジェクトが等しい場合、各オブジェクトのGetHashCodeメソッドは同じ値を返す必要があります。ただし、2つのオブジェクトが等しくない場合、2つのオブジェクトのGetHashCodeメソッドは異なる値を返す必要はありません。」

2番目の文は、「オブジェクト作成時に、等しいオブジェクトのハッシュコードは等しくなければならないという唯一のルールです」と誤解されることがよくあります。本当に理由はわかりませんが、それはここでもほとんどの答えの本質についてです。

Equalsメソッドで名前が使用されている名前を含む2つのオブジェクトを考えてみましょう。同じ名前->同じもの。インスタンスAの作成:名前= JoeインスタンスBの作成:名前= Peter

ハッシュコードAとハッシュコードBはおそらく同じではありません。インスタンスBの名前がJoeに変更されると、どうなりますか?

質問のガイドラインによると、Bのハッシュコードは変更されません。この結果は次のようになります:A.Equals(B)==> trueしかし同時に:A.GetHashCode()== B.GetHashCode()==> false。

ただし、この動作は、equals&hashcode-contractによって明示的に禁止されています。

2番目の理由:当然ですが、ハッシュコードの変更はハッシュコードを使用してハッシュリストやその他のオブジェクトを破壊する可能性がありますが、逆も同様です。ハッシュコードを変更しないと、最悪の場合、多くの異なるオブジェクトのすべてが同じハッシュコードを持ち、したがって同じハッシュビンにあるハッシュリストが取得されます。たとえば、オブジェクトが標準値で初期化される場合に発生します。


さて、ハウに目を向けると、一見すると矛盾があるようです。どちらにしても、コードは壊れます。しかし、どちらの問題もハッシュコードの変更または変更によるものではありません。

問題の原因は、MSDNで詳しく説明されています。

MSDNのハッシュテーブルエントリから:

Hashtableでキーとして使用される限り、キーオブジェクトは不変でなければなりません。

これはどういう意味ですか:

ハッシュ値を作成するオブジェクトは、オブジェクトが変更されたときにハッシュ値を変更する必要がありますが、Hashtable(または他のハッシュを使用するオブジェクト)内で使用される場合、それ自体に変更を許可することは絶対にできません。 。

まず、最も簡単な方法は、ハッシュテーブルでのみ使用する不変オブジェクトを設計することです。これは、必要に応じて通常の可変オブジェクトのコピーとして作成されます。不変オブジェクトの内部では、不変なのでハッシュコードをキャッシュしても大丈夫です。

次に、オブジェクトに「あなたは今ハッシュされています」フラグを与え、すべてのオブジェクトデータがプライベートであることを確認し、オブジェクトデータを変更できるすべての関数のフラグをチェックし、変更が許可されていない場合は例外データをスローします(つまりフラグが設定されている)。ここで、オブジェクトをハッシュ領域に配置するときは、フラグを設定し、必要がなくなったらフラグを設定解除してください。使いやすくするために、「GetHashCode」メソッド内でフラグを自動的に設定することをお勧めします-この方法は忘れられません。そして、「ResetHashFlag」メソッドの明示的な呼び出しは、プログラマがオブジェクトデータを今までに変更することが許可されているかどうかを考えなければならないことを確実にします。

わかりました、何も言わなければならない:可変データを持つオブジェクトを持つことができる場合がありますが、オブジェクトデータが変更された場合、equals&hashcode-contractに違反することなく、ハッシュコードは変更されません。

ただし、これには、equals-methodが可変データにも基づいていないことが必要です。したがって、オブジェクトを作成し、値を1回だけ計算し、それをオブジェクト内に保存して後で呼び出すときに値を返すGetHashCodeメソッドを作成する場合は、再度、絶対に、Equalsメソッドを作成する必要があります。 A.Equals(B)がfalseからtrueに変更されないように、比較のために保存された値。そうでなければ、契約は破られます。通常、この結果は、Equalsメソッドが意味をなさないことです。元の参照が等しくないが、値が等しくないことです。これは意図的な動作(つまり、顧客記録)である場合もありますが、通常はそうではありません。

したがって、オブジェクトデータが変更されたときにGetHashCodeの結果を変更し、リストまたはオブジェクトを使用するハッシュ内でのオブジェクトの使用が意図されている(または単に可能である)場合は、オブジェクトを不変にするか、読み取り専用フラグを作成してオブジェクトを含むハッシュリストのライフタイム。

(ところで:これはすべてC#または.NET固有ではありません-オブジェクトがリストにある間は、オブジェクトの識別データが決して変更されないということは、すべてのハッシュテーブル実装、またはより一般的にはインデックス付きリストの性質ですこのルールが破られると、予期せぬ予期しない動作が発生します。リスト内のすべての要素を監視し、リストの自動インデックス再作成を行うリスト実装が存在する場合がありますが、それらのパフォーマンスは確かにひどいです。

119
Alex

[〜#〜] msdn [〜#〜] から

2つのオブジェクトが等しいと比較される場合、各オブジェクトのGetHashCodeメソッドは同じ値を返す必要があります。ただし、2つのオブジェクトが等しく比較されない場合、2つのオブジェクトのGetHashCodeメソッドは異なる値を返す必要はありません。

オブジェクトのGetHashCodeメソッドは、オブジェクトのEqualsメソッドの戻り値を決定するオブジェクト状態に変更がない限り、常に同じハッシュコードを返す必要があります。これは、アプリケーションの現在の実行にのみ当てはまり、アプリケーションが再度実行されると、異なるハッシュコードが返される可能性があることに注意してください。

最高のパフォーマンスを得るには、ハッシュ関数はすべての入力に対してランダムな分布を生成する必要があります。

これは、オブジェクトの値が変更された場合、ハッシュコードが変更されることを意味します。たとえば、「Name」プロパティが「Tom」に設定されている「Person」クラスには、1つのハッシュコードと、名前を「Jerry」に変更する場合は別のコードが必要です。そうでなければ、Tom == Jerry、これはおそらくあなたが意図したものではないでしょう。


編集

またMSDNから:

GetHashCodeをオーバーライドする派生クラスは、等しいと見なされる2つのオブジェクトが同じハッシュコードを持つことを保証するためにEqualsもオーバーライドする必要があります。そうしないと、Hashtableタイプが正しく機能しない可能性があります。

MSDNのハッシュテーブルエントリ から:

Hashtableでキーとして使用される限り、キーオブジェクトは不変でなければなりません。

私がこれを読む方法は、可変オブジェクトshouldは値の変化に応じて異なるハッシュコードを返し、nlessはハッシュテーブルで使用するために設計されているということです。

System.Drawing.Pointの例では、オブジェクトは可変であり、doesはXまたはYの値が変更されたときに異なるハッシュコードを返します。これにより、ハッシュテーブルでそのまま使用することはできません。

9
Jon B

GetHashcodeに関するドキュメントは少し紛らわしいと思います。

一方では、MSDNはオブジェクトのハッシュコードは決して変わらず、一定であるべきだと述べています。他方、MSDNは、2つのオブジェクトが等しいと見なされる場合、2つのオブジェクトに対してGetHashcodeの戻り値が等しくなければならないことも述べています。

MSDN:

ハッシュ関数には次のプロパティが必要です。

  • 2つのオブジェクトが等しいと比較される場合、各オブジェクトのGetHashCodeメソッドは同じ値を返す必要があります。ただし、2つのオブジェクトが等しく比較されない場合、2つのオブジェクトのGetHashCodeメソッドは異なる値を返す必要はありません。
  • オブジェクトのGetHashCodeメソッドは、オブジェクトのEqualsメソッドの戻り値を決定するオブジェクト状態に変更がない限り、常に同じハッシュコードを返す必要があります。これは、アプリケーションの現在の実行にのみ当てはまり、アプリケーションが再度実行されると、異なるハッシュコードが返される可能性があることに注意してください。
  • 最高のパフォーマンスを得るには、ハッシュ関数はすべての入力に対してランダムな分布を生成する必要があります。

次に、これは、すべてのオブジェクトが不変であること、またはGetHashcodeメソッドが不変のオブジェクトのプロパティに基づいていることを意味します。たとえば、このクラスがあると仮定します(単純な実装):

public class SomeThing
{
      public string Name {get; set;}

      public override GetHashCode()
      {
          return Name.GetHashcode();
      }

      public override Equals(object other)
      {
           SomeThing = other as Something;
           if( other == null ) return false;
           return this.Name == other.Name;
      }
}

この実装は、MSDNにあるルールに既に違反しています。このクラスのインスタンスが2つあるとします。 instance1のNameプロパティは「Pol」に設定され、instance2のNameプロパティは「Piet」に設定されます。両方のインスタンスは異なるハッシュコードを返しますが、それらも等しくありません。ここで、instance2のNameを 'Pol'に変更し、Equalsメソッドに従って、両方のインスタンスが等しくなり、MSDNのルールの1つに従って、同じハッシュコードを返すと仮定します。
ただし、instance2のハッシュコードは変更されるため、これは実行できません。MSDNは、これは許可されていないと述べています。

次に、エンティティがある場合、そのエンティティの「プライマリ識別子」を使用するようにハッシュコードを実装できます。これは、理想的には代理キーまたは不変のプロパティです。値オブジェクトがある場合は、その値オブジェクトの「プロパティ」を使用するようにハッシュコードを実装できます。これらのプロパティは、値オブジェクトの「定義」を構成します。もちろん、これは値オブジェクトの性質です。あなたはそれがアイデンティティであることに興味がなく、むしろ価値に興味があります。
したがって、値オブジェクトは不変でなければなりません。 (.NETフレームワークにあるように、文字列、日付などはすべて不変オブジェクトです)。

頭に浮かぶもう一つのこと:
「GetHashCode」が定数値を返す必要のある「セッション」(これをどのように呼び出すべきかは本当にわかりません)。アプリケーションを開き、オブジェクトのインスタンスをDB(エンティティ)からロードし、そのハッシュコードを取得するとします。特定の数を返します。アプリケーションを閉じて、同じエンティティをロードします。今回のハッシュコードは、エンティティを最初にロードしたときと同じ値を持つ必要がありますか?私見ではありません。

9

これは良いアドバイスです。ブライアン・ペピンはこの問題について次のように述べています。

これは私を何度もつまずかせました:GetHashCodeがインスタンスのライフタイムを通して常に同じ値を返すことを確認してください。ハッシュコードは、ほとんどのハッシュテーブル実装で「バケット」を識別するために使用されることに注意してください。オブジェクトの「バケット」が変更された場合、ハッシュテーブルはオブジェクトを見つけることができない場合があります。これらは見つけるのが非常に難しいバグである可能性があるため、最初から正しく修正してください。

8
Justin R.

Marc Brooksのこのブログ投稿をチェックしてください。

VTO、RTO、およびGetHashCode()-oh、my!

さらに、フォローアップポスト(私は新しいのでリンクできませんが、初期の記事にリンクがあります)をチェックしてください。

これは、GetHashCode()実装の作成について知る必要があるすべてでした。彼は、他のユーティリティと一緒に、彼のメソッドのダウンロードも提供しています。

5
Shaun

質問に直接答えるわけではありませんが、Resharperを使用する場合は、合理的なGetHashCode実装(およびEqualsメソッド)を生成する機能があることを忘れないでください。もちろん、ハッシュコードを計算するときに、クラスのどのメンバーを考慮するかを指定できます。

5
petr k.

ハッシュコードは決して変更されませんが、ハッシュコードがどこから来たのかを理解することも重要です。

オブジェクトが値のセマンティクスを使用している場合、つまりオブジェクトのIDはその値(String、Color、すべての構造体など)によって定義されます。オブジェクトのIDがその値のすべてに依存しない場合、ハッシュコードはその値のサブセットによって識別されます。たとえば、StackOverflowエントリはデータベースのどこかに保存されます。名前またはメールアドレスを変更しても、一部の値は変更されますが(通常、長い顧客ID番号で識別されます)、顧客エントリは同じままです。

要するに:

値型のセマンティクス-ハッシュコードは値によって定義されます参照型のセマンティクス-ハッシュコードは何らかのIDによって定義されます

Eric EvansのDomain Driven Designを読むことをお勧めします。まだ意味をなさない場合は、エンティティと値のタイプ(これは上記でやろうとしていることです)について説明します。

4
DavidN

GetHashCodeのガイドラインとルール Eric Lippertによるチェックアウト

3
Ian Ringrose