Equalsメソッドは、2つのオブジェクトが等しいかを判断するために使われ、等しければtrueを、そうでなければfalseを返します。例えば、Array.IndexOfメソッドはEqualsメソッドがtrueを返す要素を探しています。ここでは、自作クラスを作成するときにEqualsメソッドをオーバーライドする方法を説明します。
ですがその前に確認に意味で、Equalsメソッドをオーバーライドしなかった時の既定の動作について説明します。(分かっているという方は読み飛ばしてください。)
まずは、Equalsメソッドをオーバーライドしない次のようなクラスを定義してみます。
'テストのためのクラス Public Class TestClass Public Message As String Public Number As Integer Public Sub New(ByVal msg As String, ByVal num As Integer) Message = msg Number = num End Sub End Class
//テストのためのクラス public class TestClass { public string Message; public int Number; public TestClass(string msg, int num) { Message = msg; Number = num; } }
Equalsメソッドをオーバーライドしない場合(EqualsメソッドがObjectクラスからそのまま継承される場合)、Equalsメソッドがどのように働くかを見てみましょう。
Dim c1 As New TestClass("こんにちは。", 0) Dim c2 As TestClass = c1 Dim c3 As New TestClass("こんにちは。", 0) Console.WriteLine(c1.Equals(c2)) 'trueを返す Console.WriteLine(c1.Equals(c3)) 'falseを返す
TestClass c1 = new TestClass("こんにちは。", 0); TestClass c2 = c1; TestClass c3 = new TestClass("こんにちは。", 0); Console.WriteLine(c1.Equals(c2)); //trueを返す Console.WriteLine(c1.Equals(c3)); //falseを返す
このように、同じオブジェクトを参照していればtrueを返しますが、そうでなければfalseを返します。つまり、参照の等価を調べた結果を返しています。
参照型の場合は上記のようになりますが、値型の場合(ValueType.Equalsメソッド)は異なります。まずは、先ほどと同じような構造体を定義して試してみます。
'テストのための構造体 Public Structure TestStruct Public Message As String Public Number As Integer Public Sub New(ByVal msg As String, ByVal num As Integer) Message = msg Number = num End Sub End Structure
//テストのための構造体 public struct TestStruct { public string Message; public int Number; public TestStruct(string msg, int num) { Message = msg; Number = num; } }
Equalsメソッドを呼び出すコードは、次のようなものです。
Dim s1 As New TestStruct("こんにちは。", 0) Dim s2 As TestStruct = s1 Dim s3 As New TestStruct("こんにちは。", 0) Console.WriteLine(s1.Equals(s2)) 'Trueを返す Console.WriteLine(s1.Equals(s3)) 'Trueを返す
TestStruct s1 = new TestStruct("こんにちは。", 0); TestStruct s2 = s1; TestStruct s3 = new TestStruct("こんにちは。", 0); Console.WriteLine(s1.Equals(s2)); //trueを返す Console.WriteLine(s1.Equals(s3)); //trueを返す
今度は、すべてのメンバが同じ値のときには、trueを返しています。値型の場合Equalsメソッドは、ビット等価(バイナリ表現が同じ)のときにtrueを返します。
このようなValueType.Equalsメソッドによる比較は非効率的ですので、Equalsメソッドによる比較が必要な構造体ではEqualsメソッドをオーバーライドすべきです。
それでは、Equalsメソッドをオーバーライドしてみましょう。Equalsメソッドでは例外をスローしてはいけないことに注意してください。
以下の例では、TestClassのNumberの値が同じならば等価となるように定義しています。
'テストのためのクラス Public Class TestClass Public Message As String Public Number As Integer Public Sub New(ByVal msg As String, ByVal num As Integer) Message = msg Number = num End Sub 'objと自分自身が等価のときはTrueを返す Public Overrides Function Equals(ByVal obj As Object) As Boolean 'objがNothingか、型が違うときは、等価でない If (obj Is Nothing) OrElse (Not Me.GetType() Is obj.GetType()) Then Return False End If 'この型が継承できないクラスや構造体であれば、次のようにできる 'If Not TypeOf obj Is TestClass Then 'Numberで比較する Dim c As TestClass = CType(obj, TestClass) Return Me.Number = c.Number 'または、 'Return (Me.Number.Equals(c.Number)) End Function 'EqualsがTrueを返すときに同じ値を返す Public Overrides Function GetHashCode() As Integer Return Me.Number End Function End Class
//テストのためのクラス public class TestClass { public string Message; public int Number; public TestClass(string msg, int num) { Message = msg; Number = num; } //objと自分自身が等価のときはtrueを返す public override bool Equals(object obj) { //objがnullか、型が違うときは、等価でない if (obj == null || this.GetType() != obj.GetType()) { return false; } //この型が継承できないクラスや構造体であれば、次のようにできる //if (!(obj is TestClass)) //Numberで比較する TestClass c = (TestClass)obj; return (this.Number == c.Number); //または、 //return (this.Number.Equals(c.Number)); } //Equalsがtrueを返すときに同じ値を返す public override int GetHashCode() { return this.Number; } }
オーバーライドしたEqualsメソッドでは、通常次のような処理を行います。
このクラスのEqualsメソッドを試してみると、次のようになります。
Dim c1 As New TestClass("こんにちは。", 0) Dim c2 As TestClass = c1 Dim c3 As New TestClass("さようなら。", 0) Console.WriteLine(c1.Equals(c2)) 'Trueを返す Console.WriteLine(c1.Equals(c3)) 'Trueを返す
TestClass c1 = new TestClass("こんにちは。", 0); TestClass c2 = c1; TestClass c3 = new TestClass("さようなら。", 0); Console.WriteLine(c1.Equals(c2)); //trueを返す Console.WriteLine(c1.Equals(c3)); //trueを返す
上記のサンプルでは、Equalsメソッドの他に、GetHashCodeメソッドもオーバーライドしていることに注目してください。通常Equalsメソッドをオーバーライドしたときは、GetHashCodeメソッドもオーバーライドします。GetHashCodeメソッドは、Hashtableなどのディクショナリコレクションで同じ値のキーを効率的に探すために使われます。
補足:「Equals() と演算子 == のオーバーロードに関するガイドライン (C# プログラミング ガイド)」では、Equalsメソッドをオーバーライドしたときは、GetHashCodeメソッドもオーバーライドすることを勧めるとされていますが、「Equals および等値演算子 (==) 実装のガイドライン」では、必ずGetHashCodeも実装するとされています。どちらが正しいのかは分かりませんが、少なくともコレクションのキーとして使用するときは、GetHashCodeもオーバーライドしないと不具合が生じる可能性があるようです。
Equalsメソッドがtrueを返すならば、GetHashCodeメソッドは同じ値を返さなければなりません。ただし、GetHashCodeメソッドが同じ値を返したとしても、Equalsメソッドがtrueになるとは限りません。
上記の例では整数型の1つのメンバのみで等価を判断しましたので、GetHashCodeメソッドではその値を返すだけでよく、簡単です。整数型でなくもっと複雑な型のオブジェクトが等価の基準になっているならば、そのオブジェクトのGetHashCodeメソッドの値を返す方法があります。
また、複数のオブジェクトを等価の基準にするときは、それぞれのGetHashCodeをXORした値を使うのが一般的です。
TestClassクラスを改造して、NumberとMessageメンバの両方が同じときに等価とする例を示します。
'テストのためのクラス Public Class TestClass Public Message As String Public Number As Integer Public Sub New(ByVal msg As String, ByVal num As Integer) Message = msg Number = num End Sub 'objと自分自身が等価のときはtrueを返す Public Overrides Function Equals(ByVal obj As Object) As Boolean 'objがNothingか、型が違うときは、等価でない If (obj Is Nothing) OrElse Not (Me.GetType() Is obj.GetType()) Then Return False End If 'NumberとMessageで比較する Dim c As TestClass = CType(obj, TestClass) Return (Me.Number = c.Number) And (Me.Message = c.Message) End Function 'EqualsがTrueを返すときに同じ値を返す Public Overrides Function GetHashCode() As Integer 'XOR Return Me.Number Xor Me.Message.GetHashCode() End Function End Class
//テストのためのクラス public class TestClass { public string Message; public int Number; public TestClass(string msg, int num) { Message = msg; Number = num; } //objと自分自身が等価のときはtrueを返す public override bool Equals(object obj) { //objがnullか、型が違うときは、等価でない if (obj == null || this.GetType() != obj.GetType()) { return false; } //NumberとMessageで比較する TestClass c = (TestClass)obj; return (this.Number == c.Number) && (this.Message == c.Message); } //Equalsがtrueを返すときに同じ値を返す public override int GetHashCode() { //XOR return this.Number ^ this.Message.GetHashCode(); } }
GetHashCodeメソッドでも、Equalsメソッドと同様に、例外をスローしてはいけません。
.NET Framework 2.0以降では、IEquatable(T)ジェネリックインターフェイスも実装した方が良いでしょう(特に構造体では)。例えIEquatable(T)ジェネリックインターフェイスを実装しなかったとしても、タイプセーフのEqualsメソッドを実装した方が良いです。
TestClassクラスにIEquatable(T)インターフェイスを実装すると、以下のようになります。
'テストのためのクラス Public Class TestClass Implements System.IEquatable(Of TestClass) Public Message As String Public Number As Integer Public Sub New(ByVal msg As String, ByVal num As Integer) Message = msg Number = num End Sub 'otherと自分自身が等価のときはtrueを返す Public Overloads Function Equals(ByVal other As TestClass) As Boolean _ Implements System.IEquatable(Of TestClass).Equals 'objがNothingのときは、等価でない If other Is Nothing Then Return False End If 'Numberで比較する Return Me.Number = other.Number End Function Public Overloads Overrides Function Equals(ByVal obj As Object) As Boolean 'objがNothingか、型が違うときは、等価でない If (obj Is Nothing) OrElse Not (Me.GetType() Is obj.GetType()) Then Return False End If Return Me.Equals(CType(obj, TestClass)) End Function 'EqualsがTrueを返すときに同じ値を返す Public Overrides Function GetHashCode() As Integer Return Me.Number End Function End Class
//テストのためのクラス public class TestClass : System.IEquatable<TestClass> { public string Message; public int Number; public TestClass(string msg, int num) { Message = msg; Number = num; } //otherと自分自身が等価のときはtrueを返す public bool Equals(TestClass other) { //objがnullのときは、等価でない if (other == null) { return false; } //Numberで比較する return (this.Number == other.Number); } public override bool Equals(object obj) { //objがnullか、型が違うときは、等価でない if (obj == null || this.GetType() != obj.GetType()) { return false; } return this.Equals((TestClass)obj); } //Equalsがtrueを返すときに同じ値を返す public override int GetHashCode() { return this.Number; } }
IEquatableを実装したときでも、Equals(Object)メソッドをオーバーライドしてください。そうでないと、TestClass型そのままと比較したときと、Object型にキャストされたTestClass型と比較したときとで別の結果になってしまいます。
補足:もしこのクラスに==演算子がオーバーロードされ、そこからIEquatable(TestClass).Equalsメソッドが呼び出されると、無限ループになる可能性があります。これを防ぐには、otherパラメータがnullかを調べるときに、object型にキャストしてから==演算子でnullと比較するか、Object.ReferenceEqualsメソッドを使用してnullと比較します。詳しくは、こちらをご覧ください。
Equalsメソッドをオーバーライドすると、Equalsメソッドの結果と等値演算子(VB.NETでは=、C#では==)の結果が異なってしまう可能性があります。よって、Equalsメソッドをオーバーライドしたならば、等値演算子もオーバーライドして同じ結果を返すようにします。等値演算子をオーバーライドする方法は、こちらで紹介しています。
ただし参照型の場合はそうではなく、ガイドライン(下記「参考」を参照)によると、ほとんどの参照型ではEqualsメソッドを実装しても等値演算子はオーバーライドすべきではないとされています。参照型でも等値演算子もオーバーライドすべきケースとして、ガイドラインでは、複素数型のような値のセマンティクスを持つ参照型、Point、String、BigNumberなどの基本型、インスタンスに含まれているデータを変更できない場合などとされています。
補足:.NET Framework 3.5や4.0のMSDNの「Equals および等値演算子 (==) 実装のガイドライン」には、「Equalsメソッドをオーバーライドする場合は、必ずIComparableも実装します」という記述があります。しかしこれは誤訳で、実際には「IComparableを実装した場合は、必ずEqualsメソッドもオーバーライドします」が正しいです。
注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。