オーバーライドする必要がある理由GetHashCode
とEquals
カスタム構造体の理由、およびデフォルトの実装が「ハッシュテーブルのキーとしての使用に適していない可能性がある」理由を説明する答えが見つからなかったため、このブログへのリンクを残しますpostは、発生した問題の実際の例で理由を説明しています。
私は投稿全体を読むことをお勧めしますが、ここに要約があります(強調と説明が追加されています)。
構造体のデフォルトのハッシュが遅く、あまり良くない理由:
CLRの設計方法、System.ValueType
またはで定義されたメンバーへのすべての呼び出しはSystem.Enum
、ボクシング割り当てを引き起こす可能性があります[...]
ハッシュ関数の実装者はジレンマに直面しています:ハッシュ関数の良い分布を作るか、それを速くするために。場合によっては、両方を実現することも可能ですが、これをで一般的に行うことは困難ValueType.GetHashCode
です。
構造体の正規ハッシュ関数は、すべてのフィールドのハッシュコードを「組み合わせ」ます。ただし、ValueType
メソッド内のフィールドのハッシュコードを取得する唯一の方法は、リフレクションを使用することです。したがって、CLRの作成者はディストリビューションで速度をトレードすることを決定し、デフォルトGetHashCode
バージョンは最初のnull以外のフィールドのハッシュコードを返し、それをタイプIDで「変更」します[...]これが適切でない場合を除き、これは妥当な動作です。たとえば、運が悪ければ、構造体の最初のフィールドの値がほとんどのインスタンスで同じであれば、ハッシュ関数は常に同じ結果を提供します。また、ご想像のとおり、これらのインスタンスがハッシュセットまたはハッシュテーブルに格納されている場合は、パフォーマンスが大幅に低下します。
[...] リフレクションベースの実装は遅いです。非常に遅い。
[...]両方ValueType.Equals
とValueType.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; }
}
デフォルトの等値実装を持つカスタム構造体を含むタプルを使用しました。そして残念ながら、構造体にはオプションの最初のフィールドがあり、ほとんどの場合[空の文字列]と同じでした。セット内の要素の数が大幅に増加して実際のパフォーマンスの問題が発生し、数万のアイテムを含むコレクションを初期化するのに数分かかるまで、パフォーマンスは問題ありませんでした。
したがって、「構造体の場合は、どのような場合に自分でパックし、どのような場合にデフォルトの実装に安全に依存できるか」という質問に答えるには、オーバーライドEquals
しGetHashCode
、カスタム構造体をハッシュテーブルのキーまたはDictionary
。ボクシングを回避するために、この場合
も実装することをお勧めしますIEquatable<T>
。
他の答えが言ったように、あなたがクラスを書いている場合、参照の等価性を使用したデフォルトのハッシュは通常は問題ないので、オーバーライドする必要がない限り、この場合は気にしませんEquals
(それに応じてオーバーライドする必要がありますGetHashCode
)。