==演算子とEqualsメソッドの違いとは?[C#].NET TIPS

.NETでは「2つのものが等しいかどうか」を比較するために==演算子、Equals/ReferenceEqualsメソッドを使える。これらの違い、使用する際の注意点を説明する。

» 2018年02月28日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載「.NET TIPS」

 C#の==演算子(および、その否定である!=演算子)とオブジェクトのEqualsメソッドは、どちらも「等しいかどうか」を調べるものだ。よく似ている2つの違いは何だろうか? 本稿では、その違いを解説するとともに、等価性を調べるコーディングの指針を提唱する。

POINT 等価性を調べるお勧めの方法

等価性を調べるお勧めの方法まとめ 等価性を調べるお勧めの方法まとめ
「ケースバイケースだ」というのが本音なのだが、一応の目安としてこのような方針を提唱したい。


 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2015以降が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。

using System;
using System.Net;
using System.Text;
using static System.Console;

本稿のサンプルコードに必要な宣言(C#)

==演算子とEqualsメソッドの違いとは?

 どちらも等価性(=同じかどうか)を調べるものだ。参照型(クラスなど)においては、既定ではどちらも参照の等価性を比較する。継承先によっては値の等価性を比較するように変更されている場合がある。==演算子とEqualsメソッドの端的な違いは、それらの実装が値の等価性比較を行うように変えられているパターンにある。つまり、.NET Frameworkクラスライブラリの設計思想にあるのだ。また、値型(.NET Frameworkが提供する構造体と列挙型)においては、どちらも値の等価性を比較するようになっている(なお、構造体を自分で定義する場合、==演算子は自分で定義しないと使用できない)。

 なお、==演算子は「オーバーロード」されるが、Equalsメソッドは(一般的に)「オーバーライド」されるという違いもある(特徴の幾つかは後述のトピックで扱う)。

 2種類の等価性には、次のような違いがある。

  • 参照の等価性:比較する2つのオブジェクト参照が同じ1つのオブジェクトを参照しているか(同一のオブジェクトか)
  • 値の等価性:比較する2つのオブジェクトが同じ値を持っているか(中身が同じか)

 どのような場合に値の等価性比較になっているかというと、次の3つのケースに分けられる。

  • 値型:Equalsメソッドは「値」の等価性比較。==演算子が実装されているなら、それも「値」の等価性比較
  • イミュータブルな参照型:==演算子もEqualsメソッドも、「値」の等価性比較
  • ミュータブルな参照型:多くは、==演算子もEqualsメソッドも「参照」の等価性比較(既定のまま)。「値」の等価性比較に意味がある(と設計者が判断した)場合は、Equalsメソッドで「値」の等価性比較を行うようになっている

 なお、上に登場した2種類の参照型とは次のようなものだ。

  • イミュータブルな参照型(変更不可能な参照型):インスタンスを生成した後はその状態を変更できない参照型のことである。Stringクラス/Uriクラス/Versionクラス(以上、System名前空間)やHttpMethodクラス(System.Net.Http名前空間)などがある
  • ミュータブルな参照型(変更可能な参照型):インスタンスの状態を後から変更できる参照型、つまり一般的なクラスのことである。その中で一部のものは、Equalsメソッドで「値」の等価性比較を行うようになっている。Cookieクラス(System.Net名前空間)やStringBuilderクラス(System.Text名前空間)などがそうだ

 さて、以上の説明からは、ミュータブルな参照型においては==演算子とEqualsメソッドの使い分けが難しそうだと予想できるだろう。さらに、Objectクラスには2引数を取るEquals静的メソッドとReferenceEqualsメソッドという等価性比較の手段もある。これではどのようにコーディングすればよいのか迷ってしまうだろう。そこで、比較するものと比較したい等価性ごとのお勧めの方法をまとめてみた(次の表)。

比較するもの 参照の等価性 値の等価性
値型 (比較する意味がない) ==演算子/!=演算子
イミュータブルな参照型 ReferenceEqualsメソッド ==演算子/!=演算子
ミュータブルな参照型 ==演算子/!=演算子
ReferenceEqualsメソッド
Equalsメソッド
【C#】比較するものと比較したい等価性ごとのお勧めの方法のまとめ

 上の表は、筆者のお勧めの方法だけを示している。表にはないが、例えばイミュータブルな参照型で参照の等価性を調べるには、object型にキャストしてから==演算子を呼び出すという方法もある。

 値型は変数ごとに必ず別のオブジェクトになるため(スタックにそのための領域が確保され、そこに値がコピーされるため)、値型については参照の等価性は比較する意味がない(比較しても必ずfalseになる)。

 「値の等価性」欄に記した方法が使えるのは、値の等価性比較が可能な場合だけである点には注意しよう。

 例えば、参照型は、既定では値の等価性を比較する方法を持っていない。その場合、==演算子もEqualsメソッドも参照の等価性比較になる。イミュータブルな参照型では、==演算子もEqualsメソッドも値の等価性比較になっているはずだ(そのため、参照の等価性比較にはReferenceEqualsメソッドを使うことになる)。

 また、値型は、構造体は既定では==演算子を持っていないが、値の等価性比較に意味があるなら==演算子を実装しているはずである(列挙型は==演算子を持っている)。構造体のEqualsメソッドの既定の実装は、リフレクションを使った値の等価性比較なので実行速度が遅い。==演算子は個別の実装であり、パフォーマンスがよいと予想されるので、構造体では(Equalsメソッドではなく)==演算子を先に試してみるべきだ。

 なお、ミュータブルな参照型で、Equalsメソッドがオーバーライドされていない場合は「参照」の等価性比較にもEqualsメソッドを使えるが、それが「値」と「参照」のどちらの等価性を比較しようとしているのかがあいまいになり、コードが分かりづらくなるので一般的には避けた方がよい。つまり、「値」の等価性比較になっている場合だけEqualsメソッドを使おうということだ(値の等価性でも参照の等価性でも構わない場合、例えばジェネリックなプロパティのセッターにおけるガード句などの場合を除く)。

 また、ミュータブルな参照型で「参照」の等価性を調べる方法をReferenceEqualsメソッドだけに限定してしまえば(つまり、==演算子は「値」の等価性比較だけに使うようにすれば)、この表はすっきりしたものになる。参照の等価性を調べる機会はLINQのおかげで減っていると思うので、そのようなコーディング規約にしてしまうのも一案かと思う。

 以降では、上の表の内容の実例と、==演算子とEqualsメソッドにまつわる幾つかのトピックを紹介しよう。

参照の等価性(同一のオブジェクトか)を調べるには?

 値型の参照は変数ごとに異なる(値型への代入は値のコピーになる)。従って、値型では参照の等価性を調べる意味はない(必ずfalseになる)。

 参照型における参照の等価性比較には、ReferenceEqualsメソッドが常に使える(ミュータブル/イミュータブルとも)。しかしそれでは長くてタイプが大変だ。==演算子のオーバーロードを持っていない参照型(ミュータブルな参照型は通常は持っていないので、つまり、ほとんどの参照型)では、参照の等価性を調べるときに==演算子を使うことが多い。また、object型にキャストすることで、==演算子は必ず参照の等価性比較になる(次のコード)。

// ミュータブルな参照型:==演算子かReferenceEqualsメソッドを使う
// なお、このStringBuilderクラスはEqualsメソッドのオーバーロードを持っているので、
// 参照の等価性比較にEqualsメソッドは使えない。
StringBuilder m1 = new StringBuilder("ABC");
StringBuilder m2 = new StringBuilder("ABC");
StringBuilder m3 = m1;
WriteLine($"m1 == m2 {m1 == m2}"); // 中身は同じだが別々のオブジェクト
// 出力:m1 == m2 False
WriteLine($"object.ReferenceEquals(m1, m2) {object.ReferenceEquals(m1, m2)}");
// 出力:object.ReferenceEquals(m1, m2) False
WriteLine($"m1 == m3 {m1 == m3}"); // 同一のオブジェクト
// 出力:m1 == m3 True
WriteLine($"object.ReferenceEquals(m1, m3) {object.ReferenceEquals(m1, m3)}");
// 出力:object.ReferenceEquals(m1, m3) True

// イミュータブルな参照型:ReferenceEqualsメソッドか、objectにキャストして==演算子
string i1 = new string('A', 10);
string i2 = new string('A', 10);
string i3 = i1;
// i1とi2は中身は同じだが別々のオブジェクト
WriteLine($"Object.ReferenceEquals(i1, i2) {object.ReferenceEquals(i1, i2)}");
// 出力:Object.ReferenceEquals(i1, i2) False
WriteLine($"(object)i1 == i2 {(object)i1 == i2}");  // objectの==演算子が呼び出される
// 出力:(object)i1 == i2 False
// i1とi3は同一のオブジェクト
WriteLine($"Object.ReferenceEquals(i1, i3) {object.ReferenceEquals(i1, i3)}");
// 出力:Object.ReferenceEquals(i1, i3) True
WriteLine($"(object)i1 == i3 {(object)i1 == i3}");
// 出力:(object)i1 == i3 True

// イミュータブルな参照型では、==演算子とEqualメソッドは値の等価性の実装になっている
WriteLine($"i1 == i2 {i1 == i2}"); // 中身は同じだが別々のオブジェクト
// 出力:i1 == i2 True
WriteLine($"i1.Equals(i2) {i1.Equals(i2)}"); // 中身は同じだが別々のオブジェクト
// 出力:i1.Equals(i2) True

参照の等価性を調べる例(C#)
常にReferenceEqualsメソッドが使える。objectにキャストしてから==演算子でもよい。
また、ミュータブルな参照型では、普通は==演算子をオーバーロードしていないので、==演算子で参照の等価性比較ができる。
Equalsメソッドは「値」の等価性比較になっていることがあるので(イミュータブルな参照型では確実にそうなっているはず)、「参照」の等価性を調べるときには使わない方がよい。

値の等価性(中身が同じか)を調べるには?

 値型とイミュータブルな参照型では、「値」の等価性は==演算子で調べるのが基本だ(ミュータブルな参照型では上で述べたように==演算子で「参照」の等価性比較を行うのが一般的)。ミュータブルな参照型で「値」の等価性比較ができるかどうかは実装次第なのだが、比較が可能な場合はEqualsメソッドを使う(次のコード)。

// 値型:==演算子を使う
char v1 = 'A';
char v2 = 'A';
char v3 = 'a';
WriteLine($"v1 == v2 {v1 == v2}"); // 'A'と'A'
// 出力:v1 == v2 True
WriteLine($"v1 == v3 {v1 == v3}"); // 'A'と'a'
// 出力:v1 == v3 False

// C#ではNullable型でも==演算子で値の等価性比較になる
int? n1 = 1;
int? n2 = 1;
WriteLine($"n1 == n2 {n1 == n2}"); // 参照の等価性ならfalseになるはずだが…
// 出力:n1 == n2 True

// イミュータブルな参照型:==演算子を使う
string i1 = new String('A', 10);
string i2 = new String('A', 10);
string i3 = new String('a', 10);
WriteLine($"i1 == i2 {i1 == i2}"); // "AAAAAAAAAA"と"AAAAAAAAAA"
// 出力:i1 == i2 True
WriteLine($"i1 == i3 {i1 == i3}"); // "AAAAAAAAAA"と"aaaaaaaaaa"
// 出力:i1 == i3 False

// ミュータブルな参照型(Equalsが値の等価性比較に実装されている場合):Equalsを使う
Cookie m1 = new Cookie("cookie1", "Value1");
Cookie m2 = new Cookie("cookie1", "Value1");
Cookie m3 = new Cookie("cookie1", "value2");
WriteLine($"m1.Equals(m2) {m1.Equals(m2)}"); // "Value1"と"Value1"
// 出力:m1.Equals(m2) True
WriteLine($"m1.Equals(m3) {m1.Equals(m3)}"); // "Value1"と"value2"
// 出力:m1.Equals(m3) False

// ミュータブルな参照型では通常は==演算子をオーバーロードしていないので、
WriteLine($"m1 == m2 {m1 == m2}"); // 参照の等価性になってしまう
// 出力:m1 == m2 False

// Equalsメソッドは、その左側がnullだと例外が出るので注意
Cookie m4 = null;
// WriteLine($"m4.Equals(m1) {m4.Equals(m1)}"); // 実行時例外
// これを避けるには次のような書き方もできる
WriteLine($"m4?.Equals(m1)??false {m4?.Equals(m1) ?? false}");
// 出力:m4?.Equals(m1)??false False

値の等価性を調べる例(C#)
値型とイミュータブルな参照型では「値」の等価性比較に==演算子が使える。ミュータブルな参照型の場合、値の等価性比較ができるように作られているなら、Equalsメソッドを使う。

==演算子はタイプセーフ

 値の等価性を調べるときは、==演算子を優先して使うとよい。値型で最も多く使われる数値型を比較するときにEqualsメソッドを書く人はいないだろう。イミュータブルな参照型ではどちらを選ぶか迷うかもしれないが、==演算子にはタイプセーフだというメリットがある(次のコード)。==演算子が提供されていないミュータブルな参照型では、Equalsメソッドを使うしかない。

Cookie m1 = new Cookie("cookie1", "Value1");
string i1 = new String('A', 10);

// ==演算子の長所:タイプセーフなので型が違うとコンパイルエラー
//WriteLine($"m1 == i1 {m1 == i1}"); // コンパイルエラー
// Equalsメソッドは型が違っても比較できてしまう
WriteLine($"m1[Cookie].Equals(i1[String]) {m1.Equals(i1)}");
// 出力:m1[Cookie].Equals(i1[String]) False

==演算子はタイプセーフなので優先的に使おう(C#)
型が違うのに値の等価性を比較する意味はない。==演算子ではコンパイルエラーになるので、無意味な比較コードを書かずに済む。Equalsメソッドでは無意味な比較コードが書けてしまう。

Equalsメソッドはポリモーフィズム

 値の等価性を調べるときに、(==演算子があっても)あえてEqualsメソッドを使うべきときもある。継承元の型、例えばobject型などとしてオブジェクトを受け取る場合だ(次のコード)。==演算子はオーバーロードなので、継承元の型にキャストしてしまうとそのオーバーロードは呼び出されない。対して、Equalsメソッドは(通常は)オーバーライドしてあるので、継承元の型にキャストされていてもオーバーライドした方のメソッドが呼び出されるのだ。

object o1 = new string('A', 10);
object o2 = new string('A', 10);

WriteLine($"o1 == o2 {o1 == o2}"); // objectの==なので参照の等価性比較になる
// 出力:o1 == o2 False
WriteLine($"o1.Equals(o2) {o1.Equals(o2)}"); // オーバーライドなのでstringのEqualsメソッド
// 出力:o1.Equals(o2) True

Equalsメソッドがオーバーライドであることを利用する例(C#)
object型にキャストしてしまうと、==演算子はobject型のもの(すなわち参照の等価性比較)になってしまう。string型ではEquals(object obj)メソッドをオーバーライドしているので、object型にキャストされていてもstring型で定義されているEquals(object obj)メソッドが呼び出される(このメソッドでは型を考慮した上で値の等価性比較が行われる)。
なお、たまにEqualsメソッドをオーバーロードだけしてオーバーライドしていないものがある(これは設計ミスだと思いたい)。例としてStringBuilderクラス(System.Text名前空間)がある。この場合は、object型にキャストするとobject型のEqualsメソッドが呼び出されてしまう(次項も参照)。

object.Equalsの落とし穴

 object型にはEquals静的メソッドもある。Equalsインスタンスメソッドと違って1つ目のオブジェクトがnullでも構わない(Equalsインスタンスメソッドで問題が起きる例をサンプルコード「値の等価性を調べる例」の末尾に示した)。

 Equals静的メソッドは、次のような順序で等価性をチェックする。

  1. 参照の等価性を比較する。同一のオブジェクト、または双方ともnullならここでtrueを返す
  2. 次に、どちらか一方だけがnullかどうか調べる。もしそうならここでfalseを返す
  3. 最後に、第1引数のオブジェクトのEqualsメソッドを呼び出して、その結果を返す

 この3で呼び出されるEqualsメソッドは、比較対象のオブジェクトが属するクラスで「オーバーライド」されたものだ。例えばobject.Equals静的メソッドで2つのstringオブジェクトを比較するときには、最終的に(object型のEquals(object obj)メソッドではなく)string型のEquals(object obj)メソッドが呼び出される(値の等価性比較になる)。

 2引数のEquals静的メソッドでは比較するオブジェクトがnullでも構わないし、比較対象のオブジェクトが属するクラスでEquals(object obj)メソッドがオーバーライドされていればちゃんと値の等価性比較を行ってくれるはずなので、とても便利である。

 ただし、StringBuilderクラスのようにEqualsメソッドのオーバーロードはあるが(例:Equals(StringBuilder sb)メソッド)、Equals(object obj)メソッドをオーバーライドしていないものでは、予期しない結果になる(次のコード)。これは2引数のEquals静的メソッドが引数をobject型で受け取り、最終的には(オーバーライドがないために)objectクラスで定義されているEquals(object obj)メソッドを呼び出し、そこで参照の等価性比較が行われるからだ(比較するオブジェクトがobject型にキャストされる)。Equals静的メソッドは慎重に使う必要がある。

string i1 = new string('A', 10);
string i2 = new string('A', 10);
WriteLine($"object.Equals(i1, i2) {object.Equals(i1, i2)}"); // 値の等価性比較
// 出力:object.Equals(i1, i2) True
// 上のコードは、((object)i1).Equals((object)i2)と同じ結果になる。
// stringのEqualsメソッドはオーバーライドされているので、
// objectにキャストされても値の等価性比較になる。

// Equals静的メソッドは、StringBuilderでは参照の等価性比較になってしまう!
StringBuilder m1 = new StringBuilder("ABC");
StringBuilder m2 = new StringBuilder("ABC");
WriteLine($"m1.Equals(m2) {m1.Equals(m2)}"); // オーバーロード(値の等価性)が呼び出される
// 出力:m1.Equals(m2) True
WriteLine($"object.Equals(m1,m2) {object.Equals(m1, m2)}"); // 参照の等価性比較になる
// 出力:object.Equals(m1,m2) False
// StringBuilderの値の等価性比較を行うEqualsメソッドはオーバーロードだけだ。
// objectにキャストされてしまうと、そのEqualsメソッドは見えなくなり、
// objectのEqualsインスタンスメソッド(参照の等価性比較)が呼び出されてしまう。

object型のEquals静的メソッドにある落とし穴(C#)
Equals静的メソッドの引数はobject型だ。つまり、比較するオブジェクトはobject型にキャストされて渡される。Equals静的メソッド内部で比較オブジェクトのEqualsインスタンスメソッドを呼び出すとき、オーバーライドされたものがあるならそちらが呼び出されるが、オーバーロードされたものは呼び出されない。比較するオブジェクトに値の等価性比較を行うEqualsメソッドがあるからといって安心していると、このStringBuilderクラスのようにEquals静的メソッドを呼び出したら参照の等価性比較になってしまって驚くことになる。値の等価性比較を行ってくれるそのEqualsメソッドはオーバーライドされたものなのかどうか、きちんと見極めておく必要があるのだ。

まとめ

 ==演算子とEqualsメソッドは、どちらも等価性を調べるものだが、比較対象(値型/イミュータブルな参照型/ミュータブルな参照型)に応じて適切に使い分ける。

利用可能バージョン:.NET Framework 全般(一部、.NET Framework 2.0以降)
カテゴリ:C# 処理対象:オブジェクト
カテゴリ:クラスライブラリ 処理対象:オブジェクト
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。