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

自作クラスの配列やコレクションでSortやBinarySearchができるようにする
IComparableインターフェイスを実装して値の大小を指定する

自作クラスの配列やコレクションをSortメソッドで並び替えたり、BinarySearchメソッドで検索する必要がある時、どのように並び替えられるようにするかを定義しておく必要があります。その方法は、IComparerを指定するか、自作クラスにIComparableインターフェイスを実装するかのどちらかです。IComparerを指定してSortメソッドで並び替える方法は「配列やコレクション内の要素を並び替える」で、BinarySearchメソッドで検索する方法は「配列やコレクション内に指定された要素があるか調べ、その位置を知る」で説明しています。ここではIComparableインターフェイスを実装する方法を説明します。

IComparableインターフェイスの実装

IComparableインターフェイスには1つのメンバCompareToメソッドしかありません。CompareToメソッドでは、自分自身とパラメータで与えられたオブジェクトを比べ、どちらが大きいかを決定します。自分のほうが小さければ負の整数を、同じならば0を、大きければ正の整数を返します。

以下にIComparableを実装した簡単なクラスの例を示します。このクラスはNameとPriceの2つのプロパティを持ちますが、Priceプロパティの大小がProductオブジェクトの大小になるようにしています。

VB.NET
コードを隠すコードを選択
'コレクションで並び替えができるクラスの定義
'IComparableインターフェイスを実装する
Public Class Product
    Implements System.IComparable

    Private _name As String
    'String型のプロパティ
    Public ReadOnly Property Name() As String
        Get
            Return Me._name
        End Get
    End Property

    Private _price As Integer
    'Int32型のプロパティ
    Public ReadOnly Property Price() As Integer
        Get
            Return Me._price
        End Get
    End Property

    'コンストラクタ
    Public Sub New(ByVal productName As String, ByVal productPrice As Integer)
        Me._name = productName
        Me._price = productPrice
    End Sub

    '自分自身がobjより小さいときはマイナスの数、大きいときはプラスの数、
    '同じときは0を返す
    Public Function CompareTo(ByVal obj As Object) As Integer _
        Implements System.IComparable.CompareTo

        'Nothingより大きい
        If obj Is Nothing Then
            Return 1
        End If

        '違う型とは比較できない
        If Not Me.GetType() Is obj.GetType() Then
            Throw New ArgumentException("別の型とは比較できません。", "obj")
        End If
        'このクラスが継承されることが無い(構造体など)ならば、次のようにできる
        'If Not TypeOf other Is TestClass Then

        'Priceを比較する
        Return Me.Price.CompareTo(DirectCast(obj, Product).Price)
        'または、次のようにもできる
        'Return Me.Price - DirectCast(obj, Product).Price
    End Function
End Class
C#
コードを隠すコードを選択
//コレクションで並び替えができるクラスの定義
//IComparableインターフェイスを実装する
public class Product : System.IComparable
{
    private string _name;
    //String型のプロパティ
    public string Name
    {
        get
        {
            return this._name;
        }
    }

    private int _price;
    //Int32型のプロパティ
    public int Price
    {
        get
        {
            return this._price;
        }
    }

    //コンストラクタ
    public Product(string productName, int productPrice)
    {
        this._name = productName;
        this._price = productPrice;
    }

    //自分自身がobjより小さいときはマイナスの数、大きいときはプラスの数、
    //同じときは0を返す
    public int CompareTo(object obj)
    {
        //nullより大きい
        if (obj == null)
        {
            return 1;
        }

        //違う型とは比較できない
        if (this.GetType() != obj.GetType())
        {
            throw new ArgumentException("別の型とは比較できません。", "obj");
        }
        //このクラスが継承されることが無い(構造体など)ならば、次のようにできる
        //if (!(other is TestClass)) { }

        //Priceを比較する
        return this.Price.CompareTo(((Product)obj).Price);
        //または、次のようにもできる
        //return this.Price - ((Product)other).Price;
    }
}
補足:このコードではnullが一番小さいとしていますが、ArgumentExceptionのような例外をスローしてもよいでしょう。

次に、このクラスのコレクションを並び替える例を示します。

VB.NET
コードを隠すコードを選択
'ArrayListの作成
Dim al As New System.Collections.ArrayList()
al.Add(New Product("キャンディ", 50))
al.Add(New Product("キャラメル", 100))
al.Add(New Product("チョコレート", 10))

'はじめの状態を表示
Console.WriteLine("並び替えなし")
For Each c As Product In al
    Console.WriteLine("Name: {0} / Price: {1}", c.Name, c.Price)
Next

'並び替える
al.Sort()
'結果を表示
Console.WriteLine("並び替え後")
For Each c As Product In al
    Console.WriteLine("Name: {0} / Price: {1}", c.Name, c.Price)
Next

'検索する
Console.WriteLine("BinarySearchで探す")
Console.WriteLine(al.BinarySearch(New Product("チョコレート", 50)))
C#
コードを隠すコードを選択
//ArrayListの作成
System.Collections.ArrayList al = new System.Collections.ArrayList();
al.Add(new Product("キャンディ", 50));
al.Add(new Product("キャラメル", 100));
al.Add(new Product("チョコレート", 10));

//はじめの状態を表示
Console.WriteLine("並び替えなし");
foreach (Product c in al)
{
    Console.WriteLine("Name: {0} / Price: {1}", c.Name, c.Price);
}

//並び替える
al.Sort();
//結果を表示
Console.WriteLine("並び替え後");
foreach (Product c in al)
{
    Console.WriteLine("Name: {0} / Price: {1}", c.Name, c.Price);
}

//検索する
Console.WriteLine("BinarySearchで探す");
Console.WriteLine(al.BinarySearch(new Product("チョコレート", 50)));

出力結果は、以下のようになります。

並び替えなし
Name: キャンディ / Price: 50
Name: キャラメル / Price: 100
Name: チョコレート / Price: 10
並び替え後
Name: チョコレート / Price: 10
Name: キャンディ / Price: 50
Name: キャラメル / Price: 100
BinarySearchで探す
1

このように、Sortメソッドを呼び出すと、Priceの大きさで並び替えられます。また、BinarySearchではNameを無視して、Priceが同じ要素が検索されます。

IComparable<T>ジェネリックインターフェイスの実装

.NET Framework 2.0以降では、さらにIComparable<T>ジェネリックインターフェイスも実装した方が良いでしょう。

先のTestClassクラスにIComparable<T>ジェネリックインターフェイスも実装した例を示します。

VB.NET
コードを隠すコードを選択
'コレクションで並び替えができるクラスの定義
'IComparable<T>ジェネリックインターフェイスを実装する
Public Class Product
    Implements System.IComparable
    Implements System.IComparable(Of Product)

    Private _name As String
    'String型のプロパティ
    Public ReadOnly Property Name() As String
        Get
            Return Me._name
        End Get
    End Property

    Private _price As Integer
    'Int32型のプロパティ
    Public ReadOnly Property Price() As Integer
        Get
            Return Me._price
        End Get
    End Property

    'コンストラクタ
    Public Sub New(ByVal productName As String, ByVal productPrice As Integer)
        Me._name = productName
        Me._price = productPrice
    End Sub

    '自分自身がotherより小さいときはマイナスの数、大きいときはプラスの数、
    '同じときは0を返す
    Public Function CompareTo(ByVal other As Product) As Integer _
        Implements System.IComparable(Of Product).CompareTo

        'Nothingより大きい
        If other Is Nothing Then
            Return 1
        End If

        'Priceを比較する
        Return Me.Price.CompareTo(other.Price)
    End Function

    Public Function CompareTo(ByVal obj As Object) As Integer _
        Implements System.IComparable.CompareTo

        'Nothingより大きい
        If obj Is Nothing Then
            Return 1
        End If

        '違う型とは比較できない
        If Not Me.GetType() Is obj.GetType() Then
            Throw New ArgumentException("別の型とは比較できません。", "obj")
        End If

        Return Me.CompareTo(DirectCast(obj, Product))
    End Function
End Class
C#
コードを隠すコードを選択
//コレクションで並び替えができるクラスの定義
//IComparable<T>ジェネリックインターフェイスを実装する
public class Product : System.IComparable, System.IComparable<Product>
{
    private string _name;
    //String型のプロパティ
    public string Name
    {
        get
        {
            return this._name;
        }
    }

    private int _price;
    //Int32型のプロパティ
    public int Price
    {
        get
        {
            return this._price;
        }
    }

    //コンストラクタ
    public Product(string productName, int productPrice)
    {
        this._name = productName;
        this._price = productPrice;
    }

    //自分自身がotherより小さいときはマイナスの数、大きいときはプラスの数、
    //同じときは0を返す
    public int CompareTo(Product other)
    {
        //nullより大きい
        if (other == null)
        {
            return 1;
        }

        //Priceを比較する
        return this.Price.CompareTo(other.Price);
    }

    public int CompareTo(object obj)
    {
        //nullより大きい
        if (obj == null)
        {
            return 1;
        }

        //違う型とは比較できない
        if (this.GetType() != obj.GetType())
        {
            throw new ArgumentException("別の型とは比較できません。", "obj");
        }

        return this.CompareTo((Product)obj);
    }
}

Equalsメソッドと演算子のオーバーライド

CompareToメソッドを定義(あるいは変更)し、Equalsメソッドをそのままにした場合、CompareToが0を返すのにEqualsがfalseを返すといったことが起こりえます。よってガイドラインでは、指定した型でIComparableインターフェイスを実装した場合は、その型でEqualsメソッドをオーバーライドするように求めています。

補足:.NET Framework 2.0 Beta1ではIComparable<T>インターフェイスにCompareToメソッドとEqualsメソッドの2つがありました。しかしCompareToメソッドはあくまで並び替えで使われるものであり、Equalsメソッドと分けるべきだなどの意見があったため、その後Equalsメソッドは分割され、IEquateable<T>インターフェイスのメソッドとなりました(詳しくは、「IComaprer & IComparable Refactoring Proposal」)。このような経緯を考慮すると、必ずしもIComparableインターフェイスを実装したならば、Equalsメソッドも実装しなければならないともいえないかもしれません。

さらに、ガイドラインでは、IComparableを実装した場合、等値(==)、非等値(!=)、小なり(<)、大なり(>)の各演算子のオーバーロードを実装することを検討するように求めています。

  • 履歴:
  • 2011/2/8 全体的な書き直し。

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

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