.NETでは、nullのハッシュコードは常にゼロである必要があります


87

セットメンバーとしてSystem.Collections.Generic.HashSet<>受け入れるようなコレクションを考えるとnull、のハッシュコードはnullどうあるべきかを尋ねることができます。フレームワークが使用しているよう0です:

// nullable struct type
int? i = null;
i.GetHashCode();  // gives 0
EqualityComparer<int?>.Default.GetHashCode(i);  // gives 0

// class type
CultureInfo c = null;
EqualityComparer<CultureInfo>.Default.GetHashCode(c);  // gives 0

これは、null許容列挙型では(少し)問題になる可能性があります。定義すると

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

その場合、Nullable<Season>(とも呼ばれますSeason?)は5つの値を取ることができますが、そのうちの2つ、つまりnullSeason.Springは同じハッシュコードを持ちます。

このような「より良い」平等比較器を書きたくなります。

class NewNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? Default.GetHashCode(x) : -1;
  }
}

しかし、のハッシュコードがnull必要な理由はあります0か?

編集/追加:

一部の人々は、これがオーバーライドについてであると考えているようですObject.GetHashCode()。実際にはそうではありません。(.NETの作成者GetHashCode()は、Nullable<>は、関連する構造体で。)パラメーターなしのユーザー作成の実装ではGetHashCode()、ハッシュコードがnull。であるオブジェクトの状況を処理することはできません。

これは、抽象メソッドのEqualityComparer<T>.GetHashCode(T)実装、またはその他の方法でインターフェイスメソッドの実装に関するものIEqualityComparer<T>.GetHashCode(T)です。さて、MSDNへのこれらのリンクを作成しているときに、これらのメソッドArgumentNullExceptionが唯一の引数がnull。これは確かにMSDNの間違いであるに違いありませんか?.NET独自の実装はいずれも例外をスローしません。その場合に投げると、に追加nullする試みが効果的に中断されますHashSet<>。アイテムHashSet<>を扱うときに何か特別なことをしない限りnull(私はそれをテストする必要があります)。

新しい編集/追加:

今、私はデバッグを試みました。でHashSet<>、私は、デフォルトの等値比較子でその値を確認することができますSeason.Springnull なり、同じバケツで終わります。これは、プライベート配列メンバーm_bucketsとを非常に注意深く調べることで判断できますm_slots。インデックスは、設計上、常に1つオフセットされていることに注意してください。

ただし、上記のコードではこれは修正されていません。結局のところ、HashSet<>値がある場合でも等値比較子を尋ねることは決してありませんnull。これは次のソースコードからのものですHashSet<>

    // Workaround Comparers that throw ArgumentNullException for GetHashCode(null).
    private int InternalGetHashCode(T item) {
        if (item == null) { 
            return 0;
        } 
        return m_comparer.GetHashCode(item) & Lower31BitMask; 
    }

これは、少なくともHashSet<>、のハッシュを変更することさえできないことをnull意味します。代わりに、解決策は次のように他のすべての値のハッシュを変更することです。

class NewerNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? 1 + Default.GetHashCode(x) : /* not seen by HashSet: */ 0;
  }
}

1
次に、非常に良い質問です。
Sachin Kainth 2012年

26
nullのハッシュコードがゼロであってはならないのはなぜですか?ハッシュ衝突は世界の終わりではありません。
Hot Licks 2012年

3
それがよく知られている、非常に一般的な衝突であることを除いて。それが悪いというわけではなく、問題の大きな問題でさえありますが、それは簡単に回避できます
Chris Pfohl 2012年

8
笑なぜ私は「.NETフレームワークが橋から飛び降りたら、それに従いますか?」と思っているのですか...
Adam Houldsworth 2012年

3
好奇心から、ヌルシーズンはどうなるでしょうか?
swDevMan81 2012年

回答:


25

nullに対して返されるハッシュコードが型に対して一貫している限り、問題はありません。ハッシュコードの唯一の要件は、等しいと見なされる2つのオブジェクトが同じハッシュコードを共有することです。

nullの場合は0または-1を返すことは、1つを選択して常に返す限り、機能します。明らかに、null以外のハッシュコードは、nullに使用する値を返さないようにする必要があります。

同様の質問:

nullフィールドのGetHashCode?

オブジェクトの識別子がnullの場合、GetHashCodeは何を返す必要がありますか?

これの「備考」 MSDNエントリ、ハッシュコードについて詳しく説明しています。痛烈、ドキュメントがヌル値のいずれかの報道や議論を提供していませんすべてではないにもコミュニティのコンテンツに- 。

列挙型の問題に対処するには、ハッシュコードを再実装してゼロ以外を返すか、nullに相当するデフォルトの「不明な」列挙型エントリを追加するか、単にnull許容の列挙型を使用しないでください。

ちなみに、面白い発見。

これに関して私が一般的に見ているもう1つの問題は、ハッシュコード4バイト以上の型を表すことができないことです。、少なくとも1回の衝突に(タイプ・サイズが増加するにつれてそれ以上)。たとえば、intのハッシュコードは単なるintであるため、intの全範囲を使用します。その範囲のどの値をnullに選択しますか?どちらを選択しても、値のハッシュコード自体と衝突します。

衝突自体は必ずしも問題ではありませんが、衝突があることを知っておく必要があります。ハッシュコードは特定の状況でのみ使用されます。MSDNのドキュメントに記載されているように、ハッシュコードは、オブジェクトごとに異なる値を返すことが保証されていないため、期待されるべきではありません。


あなたがリンクする質問は完全に似ているとは思いません。Object.GetHashCode()自分のクラス(または構造体)でオーバーライドしている場合、このコードは、ユーザーが実際にクラスのインスタンスを持っている場合にのみヒットすることを知っています。そのインスタンスはできませんnull。あなたがあなたのオーバーライド開始されない理由ですObject.GetHashCode()if (this == null) return -1;「という違いがありますnull」と、「いくつかのフィールドを持つオブジェクトであることをnull」。
Jeppe Stig Nielsen 2012

あなたが言う:明らかに、null以外のハッシュコードは、nullに使用する値を返さないはずです。それが理想的だと私は同意します。そして、それは私たちが列挙型を書くたびので、最初の場所で私の質問をした理由でT、その後、(T?)nullおよび(T?)default(T)(.NETの現在の実装では)同じハッシュコードを持つことになります。.NETの実装者がのハッシュコードnull またはのハッシュコードアルゴリズムを変更した場合、これは変更される可能性がありますSystem.Enum
Jeppe Stig Nielsen 2012

リンクがnull内部フィールド用であることに同意します。IEqualityComparer <T>用であるとおっしゃっていますが、実装ではハッシュコードはタイプに固有であるため、タイプの一貫性は同じ状況にあります。nullには型がないため、どの型のnullに対しても同じハッシュコードを返すことは重要ではありません。
Adam Houldsworth 2012年

1
注:質問を2回更新しました。(少なくともHashSet<>)では、のハッシュコードを変更することはできませんnull
Jeppe Stig Nielsen 2012

6

ハッシュコードは、等しいかどうかを判断する最初のステップとしてのみ使用され、2つのオブジェクトが等しいかどうかに関する事実上の判断として使用されることはありません(すべきではありません)。

2つのオブジェクトのハッシュコードが等しくない場合、それらは等しくないものとして扱われます(これは、誤った実装が正しいと想定しているためです。つまり、2番目に推測することはありません)。それらが同じハッシュコードを持っている場合は、実際に等しいかどうかをチェックする必要があります。nullと列挙型の値は失敗します。

結果として、ゼロを使用することは、一般的な場合の他の値と同じくらい良いです。

確かに、列挙型のように、このゼロが実際の値のハッシュコードと共有される状況があります。問題は、あなたにとって、追加の比較のごくわずかなオーバーヘッドが問題を引き起こすかどうかです。

その場合は、特定のタイプのnull許容型の場合に独自の比較子を定義し、null値が常に同じ(もちろん!)あるハッシュコードと、基になる値では生成できない値を生成するようにします。タイプ独自のハッシュコードアルゴリズム。あなた自身のタイプの場合、これは実行可能です。他の人のために-幸運:)


5

ゼロである必要はありません。必要に応じて42にすることもできます。

重要なのは、プログラムの実行中の一貫性です。

null内部的にはゼロとして表されることが多いため、これは最も明白な表現です。つまり、デバッグ中にハッシュコードがゼロの場合、「うーん、これはnull参照の問題でしたか?」と考えるように促される可能性があります。

のような数字を使用する0xDEADBEEFと、誰かがあなたがマジックナンバーを使用していると言う可能性があることに注意してください...そしてあなたはそうなるでしょう。(ゼロもマジックナンバーであると言うことができます、そしてあなたは一種の正しいでしょう...それが規則のいくらかの例外であるほど広く使われていることを除いて。)


4

良い質問。

私はこれをコーディングしようとしました:

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

次のように実行します。

Season? v = null;
Console.WriteLine(v);

戻ります null

私がそうするなら、代わりに通常

Season? v = Season.Spring;
Console.WriteLine((int)v);

0期待どおりに、またはにキャストしない場合は単純なSpringを返しintます。

だから..あなたが以下を行う場合:

Season? v = Season.Spring;  
Season? vnull = null;   
if(vnull == v) // never TRUE

編集

MSDNから

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

言い換えると、2つのオブジェクトが同じハッシュコードを持っていて、それらが等しいことを意味しない場合、 真の同等性はEqualsによって決定されます。ます。

再びMSDNから:

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


6
衝突とは、定義上、2つの等しくないオブジェクトが同じハッシュコードを持っていることを意味します。オブジェクトが等しくないことを示しました。今、彼らは同じハッシュコードを持っていますか?彼らが行うOPによると、これは衝突であることを意味します。さて、衝突が発生するのは世界の終わりではありません。nullが0以外にハッシュされた場合よりも衝突の可能性が高くなり、パフォーマンスが低下します。
Servy 2012年

1
それで、あなたの答えは実際に何を言いますか?あなたはSeason.Springがnullに等しくないと言います。まあ、それは間違いではありませんが、それは実際には質問に答えることはできません。
Servy 2012年

2
@Servy:質問によると:2つの異なるオブジェクト(nullSpring)に対して同じhascodeがあるのはなぜですか。ちなみに、同じハッシュコードを持っていても衝突の原因はなく、等しくないというのが答えです。
ティグラン

3
「答え:どうして?」さて、OPは先制的に「なぜそうしないのか」というあなたの質問に答えました。他の番号よりも衝突を引き起こす可能性が高くなります。彼は0が選ばれた理由があるかどうか疑問に思っていました、そして誰もこれまでそれに答えていません。
Servy 2012年

1
この回答には、質問の方法から明らかなように、OPがまだ知らないことは何も含まれていません。
Konrad Rudolph

4

しかし、nullのハッシュコードを0にする理由はありますか?

それはまったく何でもあったかもしれません。私は0が必ずしも最良の選択ではないことに同意する傾向がありますが、おそらくバグが最も少ないものです。

ハッシュ関数は、絶対に同じ値に対して同じハッシュを返す必要があります。これを行うコンポーネントが存在すると、これは実際にはのハッシュの唯一の有効な値ですnull。hm、のようにこれに定数がある場合object.HashOfNull、を実装する誰かがIEqualityComparerその値を使用することを知っている必要があります。彼らがそれについて考えなければ、彼らが0を使う可能性は他のどの値よりもわずかに高いと私は思います。

少なくともHashSet <>の場合、nullのハッシュを変更することさえできません

前述のように、nullのハッシュが0であるという規則にすでに従っている型が存在するという理由だけで、完全に停止することは完全に不可能だと思います。


を許可するEqualityComparer<T>.GetHashCode(T)特定の型のメソッドを実装する場合、引数が。のときに何かを行う必要があります。(1)を投げる、(2)返す、または(3)何か他のものを返すことができます。私はいつもその状況で戻るための推薦のためにあなたの答えを取りますか?TnullnullArgumentNullException00
Jeppe Stig Nielsen 2012

@JeppeStigNielsenスローとリターンについてはよくわかりませんが、リターンを選択した場合は、間違いなくゼロになります。
ローマンスターコフ2012年

2

簡単にするために0です。そのような厳しい要件はありません。ハッシュコーディングの一般的な要件を確認するだけで済みます。

たとえば、2つのオブジェクトが等しい場合、それらのハッシュコードも常に等しくなければならないことを確認する必要があります。したがって、異なるハッシュコードは常に異なるオブジェクトを表す必要があります(ただし、必ずしもその逆ではありません。2つの異なるオブジェクトが同じハッシュコードを持っている場合があります。これが頻繁に発生する場合でも、これは高品質のハッシュ関数ではありません。良好な衝突耐性)。

もちろん、私は自分の答えを数学的性質の要件に限定しました。.NET固有の技術的条件もあり、ここで読むことができます。null値の0はそれらの中にありません。


1

したがって、これはUnknown列挙値を使用することで回避できます(ただしSeason、が不明であるのは少し奇妙に思えます)。したがって、このようなものはこの問題を否定します:

public enum Season
{
   Unknown = 0,
   Spring,
   Summer,
   Autumn,
   Winter
}

Season some_season = Season.Unknown;
int code = some_season.GetHashCode(); // 0
some_season = Season.Autumn;
code = some_season.GetHashCode(); // 3

そうすると、季節ごとに一意のハッシュコード値が得られます。


1
はい、しかしこれは実際には質問に答えません。このように質問によるとnullはUknownと衝突します。違いは何ですか?
ティグラン2012年

@ Tigran-このバージョンはnull許容型を使用していません
SwDevMan81 2012年

わかりましたが、質問はnull許容型についてです。
ティグラン2012年

私はSOで何百万回もシーンを持っており、人々が答えとして改善を提案しています。
swDevMan81 2012年

1

個人的には、null許容値を使用するのは少し厄介で、できる限りそれらを避けようとします。あなたの問題はちょうど別の理由です。非常に便利な場合もありますが、私の経験則では、値型とnullを混在させないでください。これは、これらが2つの異なる世界からのものであるためです。.NET Frameworkでも同じように見えます。多くの値型TryParseは、値を値なしから分離する方法であるメソッドを提供します(null)。

特定のケースでは、独自のSeasonタイプを処理するため、問題を簡単に取り除くことができます。

(Season?)null私にとっては、一部のフィールドが不要なWebフォームがある場合のように、「季節が指定されていない」ことを意味します。私の意見ではenum、少し不格好なものを使用するよりも、その特別な「値」自体を指定する方が良いと思いますNullable<T>。読みやすくなります(ボクシングなし)(Season.NotSpecifiedvsnull)、ハッシュコードの問題を解決します。

もちろん、他のタイプの場合、int値ドメインを拡張したり、値の1つを特別なものとして指定したりすることは常に可能であるとは限りません。しかし、int?ハッシュコードの衝突は、たとえあったとしても、はるかに小さな問題です。


「ボクシング」とは、「ラッピング」、つまり構造体の中にNullable<>構造体の値を入れることを意味すると思います(HasValueメンバーはここでに設定されますtrue)。問題は本当に小さいint?ですよね?多くの場合、の値はごくわずかintであり、列挙型(理論的には多くのメンバーを持つことができます)と同等です。
Jeppe Stig Nielsen

一般に、必要な既知の値の数が限られている場合(2〜10)に列挙型が選択されると思います。制限が大きいか、まったくない場合intは、より理にかなっています。もちろん好みは異なります。
Maciej 2012年

0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2

1
それは興味深いアプローチです。特に質問の性質を考えると、回答を編集して追加の説明を含めると便利です。
ジェレミーキャニー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.