DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

自作クラスのEqualsメソッドをオーバーライドして、等価の定義を変更する

Equalsメソッドは、2つのオブジェクトが等しいかを判断するために使われ、等しければtrueを、そうでなければfalseを返します。例えば、Array.IndexOfメソッドはEqualsメソッドがtrueを返す要素を探しています。ここでは、自作クラスを作成するときにEqualsメソッドをオーバーライドする方法を説明します。

ですがその前に確認に意味で、Equalsメソッドをオーバーライドしなかった時の既定の動作について説明します。(分かっているという方は読み飛ばしてください。)

参照型のEqualsメソッドの既定の動作

まずは、Equalsメソッドをオーバーライドしない次のようなクラスを定義してみます。

VB.NET
コードを隠すコードを選択
'テストのためのクラス
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
C#
コードを隠すコードを選択
//テストのためのクラス
public class TestClass
{
    public string Message;
    public int Number;
    public TestClass(string msg, int num)
    {
        Message = msg;
        Number = num;
    }
}

Equalsメソッドをオーバーライドしない場合(EqualsメソッドがObjectクラスからそのまま継承される場合)、Equalsメソッドがどのように働くかを見てみましょう。

VB.NET
コードを隠すコードを選択
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を返す
C#
コードを隠すコードを選択
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を返します。つまり、参照の等価を調べた結果を返しています。

値型のEqualsメソッドの既定の動作

参照型の場合は上記のようになりますが、値型の場合(ValueType.Equalsメソッド)は異なります。まずは、先ほどと同じような構造体を定義して試してみます。

VB.NET
コードを隠すコードを選択
'テストのための構造体
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
C#
コードを隠すコードを選択
//テストのための構造体
public struct TestStruct
{
    public string Message;
    public int Number;
    public TestStruct(string msg, int num)
    {
        Message = msg;
        Number = num;
    }
}

Equalsメソッドを呼び出すコードは、次のようなものです。

VB.NET
コードを隠すコードを選択
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を返す
C#
コードを隠すコードを選択
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メソッドをオーバーライドしてみましょう。Equalsメソッドでは例外をスローしてはいけないことに注意してください。

以下の例では、TestClassのNumberの値が同じならば等価となるように定義しています。

VB.NET
コードを隠すコードを選択
'テストのためのクラス
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
C#
コードを隠すコードを選択
//テストのためのクラス
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メソッドでは、通常次のような処理を行います。

  1. パラメータのobjがNULLかどうかを調べ、NULLであればfalseを返す。
  2. objがこのクラス(あるいは構造体)と同じ型かを調べ、違えばfalseを返す。この時、GetTypeを使って型が同じかを調べる。
    VB.NETのTypeOf...Isや、C#のisで調べると、このクラスを継承したクラスであっても同じ型と判断されてしまう。Equalsを実装した型が継承されることのないクラスや構造体であれば、TypeOf...Isやisで判断しても良い。
    VB.NETのGetTypeや、C#のtypeofを使って調べるのも良くない。なぜなら、このクラスが継承され、Equalsがオーバーライドされたら、その派生クラスのEqualsからまず基本クラスのEqualsを呼び出して結果を確認するのが普通だが、もし基本クラスでGetTypeを使って型の確認をしたならば、値が等価であっても型が違うということでfalseを返してしまうから。
  3. 最後にobjをキャストして、プロパティを比較し、等価とするか決定する。
  4. もしこのクラスから派生したクラスを作成して、Equalsメソッドをオーバーライドしたならば、派生クラスのEqualsメソッドでは、まず基本クラスのEqualsメソッドを呼び出し、trueが返された時に派生クラスで追加されたプロパティの比較を行うだけでよい。NULLや型の確認は、基本クラスのEqualsメソッドで行われる。

このクラスのEqualsメソッドを試してみると、次のようになります。

VB.NET
コードを隠すコードを選択
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を返す
C#
コードを隠すコードを選択
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を返す

GetHashCodeメソッドのオーバーライド

上記のサンプルでは、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メンバの両方が同じときに等価とする例を示します。

VB.NET
コードを隠すコードを選択
'テストのためのクラス
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
C#
コードを隠すコードを選択
//テストのためのクラス
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メソッドと同様に、例外をスローしてはいけません。

IEquatable(T)ジェネリックインターフェイス

.NET Framework 2.0以降では、IEquatable(T)ジェネリックインターフェイスも実装した方が良いでしょう(特に構造体では)。例えIEquatable(T)ジェネリックインターフェイスを実装しなかったとしても、タイプセーフのEqualsメソッドを実装した方が良いです。

TestClassクラスにIEquatable(T)インターフェイスを実装すると、以下のようになります。

VB.NET
コードを隠すコードを選択
'テストのためのクラス
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
C#
コードを隠すコードを選択
//テストのためのクラス
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メソッドもオーバーライドします」が正しいです。
  • 履歴:
  • 2012/3/20 「Equalsメソッドをオーバーライドする場合は、必ずIComparableも実装します」が誤訳であることによる修正。(コメントでご指摘いただきました。)

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

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