web-dev-qa-db-ja.com

.NETでは、nullのハッシュコードは常にゼロでなければなりません

_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つ、つまりnullと_Season.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の作成者は、_Nullable<>_構造体のGetHashCode()のオーバーライドを行いましたが、これは関連があるですが、 )パラメータなしのGetHashCode()のユーザー作成の実装は、ハッシュコードがnullであるオブジェクトの状況を処理できません。

これは、抽象メソッド EqualityComparer<T>.GetHashCode(T) の実装、またはインターフェイスメソッド IEqualityComparer<T>.GetHashCode(T) の実装に関するものです。さて、MSDNへのこれらのリンクを作成しているときに、これらのメソッドがそれらの唯一の引数がArgumentNullExceptionである場合にnullをスローすると言っていることがわかります。これは確かにMSDNの間違いでしょうか? .NET独自の実装は例外をスローしません。この場合、スローすると、nullを_HashSet<>_に追加しようとする試みが事実上中断されます。 _HashSet<>_がnullアイテムを処理するときに異常なことをしない限り(テストする必要があります)。

新しい編集/追加:

今度はデバッグを試みました。 _HashSet<>_を使用すると、デフォルトの等値比較子を使用して、値_Season.Spring_およびnullwill同じバケツで終わります。これは、プライベート配列メンバー_m_buckets_および_m_slots_を注意深く検査することによって決定できます。インデックスは、設計上、常に1だけオフセットされていることに注意してください。

上記のコードではこれを修正できません。結局のところ、値がnullの場合、_HashSet<>_は等値比較子に問い合わせることさえありません。これは_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;
  }
}
_
86

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

Nullに対して0または-1を返すことは、1つを選択して常にそれを返す限り、機能します。明らかに、null以外のハッシュコードは、nullに使用する値を返すべきではありません。

同様の質問:

nullフィールドのGetHashCode?

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

これの「備考」 MSDNエントリ は、ハッシュコードについてさらに詳しく説明しています。感動的なことに、このドキュメントは、null値のカバレッジや議論を提供していませんまったく-コミュニティのコンテンツにもありません。

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

ちなみに興味深い発見です。

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

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

24

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

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

その結果、ゼロを使用することは、一般的な場合の他のどの値よりも優れています。

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

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

6
Andras Zoltan

しないhaveがゼロになる-必要に応じて42にすることができます。

重要なのは、プログラムの実行中にconsistencyだけです。

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

0xDEADBEEFのような数字を使用すると、マジックナンバーを使用していると誰かが言う可能性があることに注意してください。 (ゼロも魔法の数だと言えるかもしれませんが、それはまあまあです。ただし、ルールの例外となるほど広く使用されている点を除きます。)

5
user541686

良い質問。

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

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

これを次のように実行します。

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

nullを返します

もしそうなら、代わりに通常

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

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

したがって、次のようにした場合:

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

[〜#〜]編集[〜#〜]

From [〜#〜] msdn [〜#〜]

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

つまり、2つのオブジェクトが同じであることを意味しない同じハッシュコードを持っている場合、realの等価性はEquals

MSDNから再び:

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

4
Tigran

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

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

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

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

上で述べたように、nullのハッシュが0であるという慣例に従っている型が既に存在するからといって、完全に不可能になるとは思いません。

4
Roman Starkov

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

たとえば、2つのオブジェクトが等しい場合、それらのハッシュコードも常に等しい必要があります。したがって、異なるハッシュコードは常に異なるオブジェクトを表す必要があります(ただし、必ずしもその逆ではありません)。2つの異なるオブジェクトが同じハッシュコードを持っている可能性があります。良好な衝突抵抗)。

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

2
Thomas Calc

個人的には、null許容値を使用するのは少し厄介で、できる限り回避するようにしています。あなたの問題はもう一つの理由です。場合によっては非常に便利なこともありますが、私の経験則では、2つの異なる世界からの値であるという理由だけで、可能であれば値型とnullを混在させないようにしています。 .NETフレームワークでは、それらは同じように見えます-多くの値タイプは、値を値なし(TryParse)から分離する方法であるnullメソッドを提供します。

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

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

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

1
Maciej

したがって、これは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
SwDevMan81