【C#】クラスや構造体に==演算子やEquals()を書く時の注意点

たまに==演算子オーバーロードしたりEquals()をオーバーライドしたりしますが、
ちゃんとやろうとすると意外とややこしいのでメモです。

実用性重視。細かい話には踏み込みません。

デフォルトの実装

==もEquals()も、二つのオブジェクトが等しいことをチェックするためのものです。
ではなにをもって二つのオブジェクトを「等しい」と判定するのかというと以下の2パターンがあります。

  • すべてのメンバの値が等しい
  • 参照が等しい

これを踏まえて==とEquals()のデフォルトの実装をみてみます。
デフォルトの実装はクラスと構造体で異なります。

クラスの場合、デフォルトで==もEquals()も実装されています。
そして==もEquals()も「参照が等しい」ことをチェックします。

構造体の場合、デフォルトではEquals()のみが実装されています。
このEquals()は「すべてのメンバの値が等しい」ことをチェックします。
==は実装されていないので、使いたければオーバーロードする必要があります。

==とEquals()の違い

では==とEquals()はどのように使い分けるのでしょうか。

構造体の場合は、==もEquals()もすべてのメンバの値が等しいことを示すべきです。
つまり、原則として==とEquals()に違いがあるべきではありません。
ただし前節の通り==は定義されていないので、使いたければ定義する必要があります。

クラスの場合、==は常に参照が等しいことを示すべきであり、すなわちオーバーロードすべきではありません。
ただしクラスで、すべてのメンバの値が等しいことをチェックしたい場合があります。
そのようなときにはEquals()をオーバーライドし、値をチェックします。
よってクラスでは==は参照のチェックに、Equals()は実装次第で参照のチェックあるいは値のチェックに使われます。

==を書いたらEquals()も書くべき?その逆は?

次に==をオーバーロードしたらEquals()もオーバーライドするべきなのかを考えます。
==をオーバーロードするのは構造体で==演算子を使いたい場合でした。
この場合、上述の通り==とEquals()は同じ結果を返すべきなので、==を定義したらEquals()も定義して同じ結果を返すべきです。

一方、Equals()をオーバーライドするのはクラスでメンバの値が等しいかチェックをしたいときでした。
上述の通り、Equals()でメンバの値をチェックしても==は参照の等価をチェックするべきなので、==はオーバーロードするべきではありません。
つまりEquals()をオーバーライドしたとしても==はオーバーロードしなくていいです。

GetHashCode()も書くべき?

よく==やEquals()と一緒にGetHashCode()もオーバーライドされます。
このメソッドはハッシュコードを返します。

二つのオブジェクト間でこのハッシュコードが異なればオブジェクトが等しくないことが保証されます。
ただし、値が同じであっても必ずしもオブジェクトが等しいとは限りません。

オブジェクトが等しいかどうかを判定する際のヒントとなる程度のもののようです。

ただ、コレクションのキーとして使われる可能性があることを考えると、
Equalsや==をオーバーライドした際には必ず実装しておくべきであるようです。

「Equals() と演算子 == のオーバーロードに関するガイドライン (C# プログラミング ガイド)」では、Equalsメソッドをオーバーライドしたときは、GetHashCodeメソッドもオーバーライドすることを勧めるとされていますが、「Equals および等値演算子 (==) 実装のガイドライン」では、必ずGetHashCodeも実装するとされています。どちらが正しいのかは分かりませんが、少なくともコレクションのキーとして使用するときは、GetHashCodeもオーバーライドしないと不具合が生じる可能性があるようです。
https://dobon.net/vb/dotnet/beginner/equals.html より引用

結論1: 値型に==をオーバーロードしたい場合

長くなったのでまとめます。
まず値型に==をオーバーロードしたい場合にやるべきことは下記の通りです。

  1. ==をオーバーロード
  2. Equals()をオーバーライド(==と同じ結果を返す)
  3. GetHashCode()をオーバーライド
  4. !=をオーバーロード

最後の!=演算子については記事中で触れませんでしたが、
==をオーバーロードする場合には必ずオーバーロードする必要があります。

結論2: 参照型を値で比較したい場合

次に参照型で値を比較したい場合には次のような実装をします。

  1. Equals()をオーバーライドして値の等しさを返す
  2. GetHashCode()をオーバーライド

参考

自作クラスのEqualsメソッドをオーバーライドして、等価の定義を変更する - .NET Tips (VB.NET,C#...)

C#における、==演算子のオーバーロードおよびEqualsメソッドの実装について... - Yahoo!知恵袋