DOBON.NET

2つの値が等しいか調べる、等値演算子(==)とEqualsメソッドの違い

ここでは、2つの値が等しいか(等価か)を調べる方法について説明します。なお2つの文字列が等しいかを調べる方法については、「2つの文字列が等しいかを調べる」で説明します。

もし「値型」と「参照型」の言葉の意味が分からない場合は、まず「値型と参照型の区別と違い」をご覧ください。

2種類の等価

実は、「等価」と言っても2種類あります。それは「値の等価」と「参照の等価」です。

「値の等価」とは、比較する2つのオブジェクトの中身が同じという意味です。「中身が同じ」という言い方はあいまいですが、どのようなときに等価なのかという定義がされており、それに従って等価であるという意味です。例えば、System.Drawing.Size構造体では、HeightとWidthの両方のプロパティが同じときに等価であると定義されています。

「参照の等価」とは、比較する両者が同じインスタンスを参照しているという意味です。

つまり、値型の比較で等価といった場合は、必然的に「値の等価」となります。参照型の比較で等価といった場合は、「値の等価」または「参照の等価」のどちらかになります。

等価の調べ方

2つの値が等価であるかを調べるには、等値演算子(VB.NETでは=、C#では==)、または、Equalsメソッドのどちらかを使います。

補足:比較する値の型がIComparableインターフェイスを実装している場合は、CompareToメソッドが0を返すかによって等価を調べることもできます。ただし、「自作クラスの配列やコレクションでSortやBinarySearchができるようにする」で説明したように、基本的にCompareToは並べたときの順番を定義したものですので、等価の確認には使用しないほうが良いでしょう。

VB.NETでは、「値の等価」を調べるには=演算子を、「参照の等価」を調べるにはIs演算子を使います。通常は、値型の比較には=演算子、参照型の比較にはIs演算子しか使うことができません。しかし、String型のように、クラスで等値演算子がオーバーロードされていれば、参照型でも=演算子を使って「値の等価」を調べることができます。

補足:値型であってもnull許容型の場合は、Nothingとの比較にIs演算子を使用します。

C#では、値型の比較に==演算子を使うと「値の等価」を調べることになります。参照型の比較に==演算子を使うと、通常は「参照の等価」を調べます。しかし、String型のように、クラスで等値演算子がオーバーロードされているならば、参照型でも==演算子で「値の等価」を調べます。

Equalsメソッドは、値型の比較に使うと、「値の等価」を調べます。参照型の比較に使うと、通常は「参照の等価」を調べます。しかし、String型のように、クラスのEqualsメソッドがオーバーライドされていれば、参照型でも「値の等価」を調べます。

補足:静的メソッドのObject.Equals(Object, Object)メソッドは、2つのオブジェクトがどちらもNULLでなければ、一番目のパラメータに渡されたオブジェクトのEqualsメソッドを呼び出した結果を返します。

「参照の等価」を調べるためには、Object.ReferenceEqualsメソッドを使用することもできます。さらにC#では、Object型にキャストしてから==演算子で比較することでも、確実に参照の等価を調べることができます。

等値演算子とEqualsメソッドで値の等価を調べることができるクラス(等値演算子がオーバーロードされ、かつ、Equalsメソッドがオーバーライドされているクラス)は多くありません。その代表は、Stringクラスです。その他にもVersionクラスなどもそのようですが、とりあえずStringクラスはこのように特別なクラスであることを覚えておいてください。このようなクラスでは、参照型にもかかわらず、等値演算子やEqualsメソッドで「値の等価」を調べることができます。

補足:等値演算子を実装する方法は「自作クラスの演算子をオーバーロードする」、Equalsメソッドを実装する方法は「自作クラスのEqualsメソッドをオーバーライドして、等価の定義を変更する」をご覧ください。

等値演算子やEqualsメソッドを使って等価を調べる例を以下に示します。

VB.NET
コードを隠すコードを選択
'値型の等価を調べる
Dim i1 As Integer = 1
Dim i2 As Integer = i1 * i1
Console.WriteLine(i1 = i2) 'True
Console.WriteLine(i1.Equals(i2)) 'True

'参照型の等価を調べる
'o1とo2は別のインスタンス
Dim o1 As New Object()
Dim o2 As New Object()
Console.WriteLine(o1 Is o2) 'False
Console.WriteLine(o1.Equals(o2)) 'False
'(o1 = o2)は不可

'o1とo2は同じインスタンス
o2 = o1
Console.WriteLine(o1 Is o2) 'True
Console.WriteLine(o1.Equals(o2)) 'True

'String型の等価を調べる
's1とs2は同じ値だが、別のインスタンス
Dim s1 As New String("a"c, 10)
Dim s2 As New String("a"c, 10)
Console.WriteLine(s1 = s2) 'True
Console.WriteLine(s1.Equals(s2)) 'True
Console.WriteLine(s1 Is s2) 'False
Console.WriteLine(Object.ReferenceEquals(s1, s2)) 'False
C#
コードを隠すコードを選択
//値型の等価を調べる
int i1 = 1;
int i2 = i1 * i1;
Console.WriteLine(i1 == i2); //true
Console.WriteLine(i1.Equals(i2)); //true

//参照型の等価を調べる
//o1とo2は別のインスタンス
object o1 = new object();
object o2 = new object();
Console.WriteLine(o1 == o2); //false
Console.WriteLine(o1.Equals(o2)); //false

//o1とo2は同じインスタンス
o2 = o1;
Console.WriteLine(o1 == o2); //true
Console.WriteLine(o1.Equals(o2)); //true

//String型の等価を調べる
//s1とs2は同じ値だが、別のインスタンス
string s1 = new string('a', 10);
string s2 = new string('a', 10);
Console.WriteLine(s1 == s2); //true
Console.WriteLine(s1.Equals(s2)); //true
Console.WriteLine(object.ReferenceEquals(s1, s2)); //false

等値演算子とEqualsメソッドの違い

等値演算子とEqualsメソッドは、同じ結果を返して当然だと思っている方も多いかもしれません。確かに値型ではよほどのことがない限り(設計ミスなど)、同じ結果となります。しかし参照型の場合は、そうとは限りません。特にC#の場合は、==演算子が値の等価を調べているのか、参照の等価を調べているのかが分かりにくいため、間違えてしまう可能性が十分あります。

ここからは等値演算子とEqualsメソッドの違いと注意点について考えます。少し難しい話になるかもしれませんので、分からないという方は、とりあえず結論をご覧ください。

ポリモーフィズム

When should I use == and when should I use Equals?」によると、等値演算子とEqualsメソッドの最大の違いは、ポリモーフィズムだということです。つまり、等値演算子はオーバーロードされるが、Equalsメソッドはオーバーライドされるという点が異なります。

この違いによって、等値演算子とEqualsメソッドの結果が異なる可能性があります。ただしVB.NETでは、この違いを知らないがために失敗をするというケースはほとんどないと思いますので、まずはC#について説明し、後におまけとしてVB.NETの説明をします。

早速具体例を示します。「When should I use == and when should I use Equals?」で紹介されているサンプルそのままです。

C#
コードを隠すコードを選択
//内容は等しいが、異なるオブジェクトのStringを2つ作る
string a = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });
string b = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });

Console.WriteLine(a == b); //true
Console.WriteLine(a.Equals(b)); //true

//Object型にする
object c = a;
object d = b;

Console.WriteLine(c == d); //false
Console.WriteLine(c.Equals(d)); //true

この例で「c.Equals(d)」はtrueを返しますが、「c == d」はfalseを返します。これは、「c == d」がStringクラスでオーバーロードされたString型同士の等値演算子を使わずに、Objectクラスから継承したObject型同士の等値演算子を使うためです。Object型同士の等値演算子は参照の等価を調べるため、falseを返します。

これに対して、「c.Equals(d)」はStringクラスでオーバーライドされたEqualsメソッドを呼び出しているため、値の等価を調べたことになり、trueを返します。

補足:意味がさっぱり分からないという方は、演算子をオーバーロードする方法と、Equalsメソッドをオーバーライドする方法をご覧ください。

VB.NETでは、Object型の比較に=演算子が使えませんので、上記のC#のコードような問題は起こりません。しかしポリモーフィズムの違いによる問題が全く起こらないというわけではありません。以下に、少し強引に問題を起こす例を紹介します。

まずは次のような2つのクラスを作成します。TestClass1ではEqualsメソッドと等値演算子を実装し、Messageの値が同じときに等価とします。TestClass2はTestClass1から派生し、Numberの値が同じときに等価とします。(コードを簡単にするため、Nothingと型の確認を省略しています。詳しくは、演算子をオーバーロードする方法Equalsメソッドをオーバーライドする方法をご覧ください。)

VB.NET
コードを隠すコードを選択
Public Class TestClass1
    Public Message As String

    Public Sub New(ByVal msg As String)
        Me.Message = msg
    End Sub

    'Equalsメソッドをオーバーライドする
    Public Overrides Function Equals(ByVal obj As Object) As Boolean
        Return Me = DirectCast(obj, TestClass1)
    End Function

    '比較演算子の=と<>をオーバーロードする
    Public Shared Operator =( _
        ByVal c1 As TestClass1, ByVal c2 As TestClass1) As Boolean
        Return c1.Message = c2.Message
    End Operator

    Public Shared Operator <>( _
        ByVal c1 As TestClass1, ByVal c2 As TestClass1) As Boolean
        Return Not (c1 = c2)
    End Operator
End Class

Public Class TestClass2
    Inherits TestClass1
    Public Number As Integer

    Public Sub New(ByVal msg As String, ByVal num As Integer)
        MyBase.New(msg)
        Me.Number = num
    End Sub

    'Equalsメソッドをオーバーライドする
    Public Overrides Function Equals(ByVal obj As Object) As Boolean
        Return Me = DirectCast(obj, TestClass2)
    End Function

    '比較演算子の=と<>をオーバーロードする
    Public Overloads Shared Operator =( _
        ByVal c1 As TestClass2, ByVal c2 As TestClass2) As Boolean
        Return c1.Number = c2.Number
    End Operator

    Public Overloads Shared Operator <>( _
        ByVal c1 As TestClass2, ByVal c2 As TestClass2) As Boolean
        Return Not (c1 = c2)
    End Operator
End Class

そして、次のようなコードを実行してみてください。

VB.NET
コードを隠すコードを選択
Dim c1 As New TestClass2("あ", 1)
Dim c2 As New TestClass2("あ", 2)

'False
Console.WriteLine(c1 = c2)
'False
Console.WriteLine(c1.Equals(c2))

'TestClass1型にする
Dim c3 As TestClass1 = c1
Dim c4 As TestClass1 = c2

'True
Console.WriteLine(c3 = c4)
'False
Console.WriteLine(c3.Equals(c4))

「c3 = c4」はTrueとなりますが、「c3.Equals(c4)」はFalseになります。これは、「c3 = c4」がTestClass1クラスから継承したTestClass1型同士の等値演算子の定義を使用しているのに対して、「c3.Equals(c4)」はTestClass2でオーバーライドしたEqualsメソッドを呼び出しているからです。

しかしほとんどのクラスでは、等値演算子の実装でEqualsメソッドを呼び出しているため、このようなことは起こりません。

参照型でEqualsメソッドだけオーバーライドされている可能性

値型の場合は、Equalsメソッドがオーバーライドされていれば、同じ結果を返すように、等値演算子もオーバーロードされていると考えてよいでしょう(もちろん、そうなっていない可能性もありますが)。

しかし参照型の場合は、Equalsメソッドがオーバーライドされていても、等値演算子がオーバーロードされているかは分かりません。なぜなら、ほとんどの参照型では、Equalsメソッドをオーバーライドしても等値演算子はオーバーロードするなとガイドラインに書かれているからです。詳しくは、こちらをご覧ください。

よって参照型でEqualsメソッドがオーバーライドされていれば、Stringのように特別な型を除いては、Equalsメソッドは値の等価を返し、等値演算子は参照の等価を返すと考えた方が良いでしょう。

VB.NETの場合は、=演算子が実装されていなければ=による比較はできませんので、この問題を気にする必要は無いでしょう。

一つ例を示します。同じ値を持ったStringBuilderを等値演算子とEqualsメソッドで比較してみましょう。

VB.NET
コードを隠すコードを選択
Dim s1 As New System.Text.StringBuilder("こんにちは")
Dim s2 As New System.Text.StringBuilder("こんにちは")

'Console.WriteLine(s1 = s2) 'コンパイルエラーとなる
Console.WriteLine(s1 Is s2) 'false
Console.WriteLine(s1.Equals(s2)) 'true 
C#
コードを隠すコードを選択
System.Text.StringBuilder s1 = new System.Text.StringBuilder("こんにちは");
System.Text.StringBuilder s2 = new System.Text.StringBuilder("こんにちは");

Console.WriteLine(s1 == s2); //false
Console.WriteLine(s1.Equals(s2)); //true

StringBuilderクラスには等値演算子が実装されていないため、==演算子では参照の等価を調べたことになります。しかしEqualsメソッドは実装されており、値の等価を調べることができます。よって上記の例のように、==演算子とEqualsメソッドで違う結果になります。

タイプセーフ

Equals を使うな。使う事を推奨するな。」などで指摘されていますが、等値演算子がタイプセーフであるのに対して、Equalsメソッドはタイプセーフではありません。このため、Equalsメソッドではボックス化が行われ、パフォーマンスが損なわれる可能性があります。

ただし.NET Framework 2.0からは、IEquatable(T)ジェネリックインターフェイスの登場もあり、ほぼすべての値型でタイプセーフのEqualsメソッドが使えると思ってよいでしょう。

しかし裏を返せば、タイプセーフである等値演算子にはできない異なる型の比較もEqualsメソッドではできるということです。ただし型が異なればEqualsメソッドはfalseを返すはずですので、はじめから型が異なると分かっている場合は比較するだけ無駄です。

構造体は実装しないと等値演算子で比較できない

等値演算子が実装されていない構造体では、等値演算子を使って値の等価を調べることができません。しかしEqualsメソッドを使えば、Equalsメソッドがオーバーライドされていないとしても、値の等価を調べることができます。この場合に使われるEqualsメソッドはValueTypeから継承されたもので、ビット等価(バイナリ表現が同じ)のときにtrueを返します。詳しくは、「値型のEqualsメソッドの既定の動作」をご覧ください。

どちらが速いか

等値演算子とEqualsメソッドのどちらで等価を調べるのがより速いかについては、型にこれらがどのように実装されているかによって異なるため、一概には言えません。しかし、String以外の組み込み型(Int32、Single、Boolean、Objectなど)では、等値演算子の方が速いようです(私が試したわけではありません)。Stringや他の参照型では、通常等値演算子の実装でEqualsメソッドを呼び出しているため、Equalsメソッドの方が早いようです(特にString型については、「等値演算子とEqualsメソッドのどちらが速いか?」で詳しく説明しています。)

ただし、この違いは保障されたものではありませんし、気にするほど速さが異なるとも思えません。

結局、どちらを使うべきか

VB.NETでは、値の等価は=演算子で調べ、参照の等価はIs演算子で調べるのが良いでしょう。ただし参照型の中にはEqualsメソッドでしか値の等価を調べることができないものもありますので、その場合はEqualsメソッドを使います。

C#では、値型の等価は==演算子で調べるのが良いでしょう。参照型で値の等価を調べるには、Equalsメソッドを使うのが確実でしょう。参照型で明確に参照の等価を調べたいならば、Object.ReferenceEqualsメソッド(またはObject型にキャストしてから==演算子)を使います。

参照型の等価を調べるときは、どの方法を使うにしても、参照の等価を調べたいのか、値の等価を調べたいのかをはっきりさせて、適切な方法を選択するようにしてください。

  • 履歴:
  • 2013/7/2 「ポリモーフィズム」のVB.NETのサンプルで、TrueとFalseの結果が逆になっていたのを修正。(コメントでご指摘頂きました。)

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • .NET Tipsをご利用いただく際は、注意事項をお守りください。
共有する

この記事への評価

この記事へのコメント

この記事に関するコメントを投稿するには、下のボタンをクリックしてください。投稿フォームへ移動します。通常のご質問、ご意見等は掲示板へご投稿ください。