ここでは、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メソッドを使って等価を調べる例を以下に示します。
'値型の等価を調べる 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
//値型の等価を調べる 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メソッドは、同じ結果を返して当然だと思っている方も多いかもしれません。確かに値型ではよほどのことがない限り(設計ミスなど)、同じ結果となります。しかし参照型の場合は、そうとは限りません。特に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?」で紹介されているサンプルそのままです。
//内容は等しいが、異なるオブジェクトの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メソッドをオーバーライドする方法をご覧ください。)
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
そして、次のようなコードを実行してみてください。
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メソッドがオーバーライドされていれば、Stringのように特別な型を除いては、Equalsメソッドは値の等価を返し、等値演算子は参照の等価を返すと考えた方が良いでしょう。
VB.NETの場合は、=演算子が実装されていなければ=による比較はできませんので、この問題を気にする必要は無いでしょう。
一つ例を示します。同じ値を持ったStringBuilderを等値演算子とEqualsメソッドで比較してみましょう。
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
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型にキャストしてから==演算子)を使います。
参照型の等価を調べるときは、どの方法を使うにしても、参照の等価を調べたいのか、値の等価を調べたいのかをはっきりさせて、適切な方法を選択するようにしてください。
注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。