ソートを行うためには、並べ替えを行う個々の要素についてその大小関係を調べる必要があります。 また、独自に作成したクラスをソートするためには、その大小関係を定義する必要があります。
ここでは、コレクションとソートの基本と、大小関係を定義したり比較するためのインターフェイスであるIComparable・IComparerおよび関連するクラスについて見ていきます。 また、比較演算子のオーバーロードとこれらのインターフェイスの実装についても触れています。 ここで解説するソート方法やDictionaryのソートなどについてはソートでも解説しているので、必要に応じて参照してください。
コレクションとソート
.NET Frameworkでは、ソートを行うためのメソッドとしてArray.Sort、ArrayList.Sort、List<T>.Sortなどのメソッドが用意されています。
Array.Sortメソッドは引数で指定された配列のソートを行い、ArrayList.SortメソッドとList<T>.Sortメソッドはインスタンス内に格納されている要素をソートします。 これらのメソッドは、いずれもクイックソートを行います。 以下のコードは、これらソートを行うメソッドを使った例です。
これらのメソッドでは、引数でIComparerインターフェイスやComparisonデリゲートなどを指定することで並べ替えの際の動作を変更することができるようになっています。 詳細については後述していきます。
ソートできる型
先の例では、文字列型の値をコレクションに格納してソートしましたが、int、double、DateTimeなど他の基本型でも同様にソートすることが出来ます。
ソートできない型
このように、基本型ならばソートは可能なように見えます。 どのような型がソート可能となり、ソート可能とならないのか、その違いを見るために文字列をラップするクラスMyStringを作成し、先の例と同様にソートしてみます。
実行結果を見て分かる通り、Sortメソッドを呼び出した時点で例外がスローされ、ソートは出来ませんでした。 何が原因でソート出来なかったのか、スローされた例外について詳しく見みます。 スローされた例外はInvalidOperationExceptionで、例外のメッセージは次のようになっています。
つまり、配列(コレクション)内にある要素を比較しようとしたが、その要素がIComparableインターフェイスを実装していないことを理由にArgumentExceptionをスローしたために、InvalidOperationExceptionとなったことがソート出来なかった原因となります。 言い換えると、ソートを行うには、その要素がIComparableを実装している必要があるということです。 ここまで例に挙げたstring、int、double、DateTimeは、すべてIComparableを実装しているためソートが可能だったのです。
IComparable
それでは、ソートを行う上で必要とされるIComparableインターフェイス(System名前空間)を実装するためにIComparableについて詳しく見ていきます。 IComparableインターフェイスを実装する場合は一つのメソッドCompareToを実装しなければなりません。 このメソッドでは、一つのobject型の引数を取り、インターフェイスを実装するインスタンスと引数のオブジェクトとの大小関係に応じて次の値を返す必要があります。
インスタンスと引数objの大小関係 | 戻り値として返す値 |
---|---|
並べ替えたときに、現在のインスタンスの方が引数objよりも前となる (thisはobjよりも小さい) |
0より小さい値 |
並べ替えたときに、現在のインスタンスと引数objは同じ位置となる (thisはobjと等しい) |
0 |
並べ替えたときに、現在のインスタンスの方が引数objよりも後となる (thisはobjよりも大きい) |
0より大きい値 |
早速、上記の仕様を満たすようIComparableを実装したクラスを用意し、ソートしてみます。 次のコードでは、メンバにIDと名前を持つクラスAccountを作成し、IComparableを実装しています。
結果を見てわかるとおり、ソートの際にInvalidOperationExceptionがスローされることもなく、Accountクラスのidフィールドの値にしたがってソート出来ていることが分かると思います。
この例ではArray.Sortメソッドを使ったため、Accountクラスのインスタンス同士のみの比較しか行われませんが、ArrayListなどを使う場合はすべて同じ型同士の比較になるとは限りません。 IComparable.CompareToメソッドは引数がobject型であるため、どのような型との比較になるかは呼び出し元次第となります。 そのため、IComparableインターフェイスを実装する側で引数の型をチェックする必要があります。 上記のコードでは異なる型との比較ではArgumentExceptionをスローするようにしていますが、場合によっては例外とはせず適切な比較結果を返すように実装する事も可能です。
また、null(Nothing)との比較についても、上記のコードではArgumentNullExceptionをスローするようにしていますが、string型のようにnullはすべてのオブジェクトよりも小さいと定義することもできます。
IComparer
IComparableに似たインターフェイスとして、.NET FrameworkにはIComparerインターフェイス(System.Collections名前空間)が用意されています。 IComparableがインターフェイスを実装するオブジェクトと他のオブジェクトとの比較処理を提供するのに対し、IComparerは任意の二つのオブジェクトの比較処理を提供するためのものです。 言い換えると、IComparableでは比較処理は常に比較される対象と結びついていますが、IComparerでは比較処理と比較される対象を分離させることが出来ます。
このことにどのような利点があるかというと、例えば先に例に挙げたAccountクラスでは名前とIDのフィールドを持っていますが、IComparableで実装したのはIDによる比較のみです。 これを名前順やIDの降順で並べ替えたりしたい場合、IComparableの実装を書き換える必要が出てきますが、IComparerを使うことで実装を書き換えずに新たな比較処理を個別に用意することができるようになります。
IComparerインターフェイスを実装する場合は一つのメソッドCompareを実装しなければなりません。 IComparable.CompareToメソッドが一つのobject型の引数を取るのに対し、IComparer.Compareメソッドでは二つのobject型の引数を取ります。 戻り値は、IComparable.CompareToメソッドと同様、引数同士の大小関係に応じて次の値を返す必要があります。
引数xとyの大小関係 | 戻り値として返す値 |
---|---|
並べ替えたときに、引数xが引数yよりも前となる (xはyよりも小さい) |
0より小さい値 |
並べ替えたときに、引数xが引数yと同じ位置となる (xはyと等しい) |
0 |
並べ替えたときに、引数xが引数yよりも後となる (xはyよりも大きい) |
0より大きい値 |
早速、上記の仕様を満たすようIComparerを実装したクラスを用意し、ソートしてみます。 次のコードでは、IComparerを実装してint型の配列を昇順・降順で並べ替えるためのクラスを作成し、ソートに使っています。
Array.Sortメソッドでは引数にIComparerを指定すると、そのIComparerを使って並べ替えが行われるようになります。 その結果、上記のように昇順・降順でのソートが行えるようになります。 なお、上記のコードでは、比較のためにIComparerを使わずArray.SortメソッドとArray.Reverseメソッドを使って降順にソートする例も記述しています。
もう少し違った例を挙げてみます。 先ほどのAccountクラスと、これを比較するためのAccountComparerクラスを用意してソートします。 AccountComparerには、比較の際に昇順・降順および名前順・ID順をオプションとして選べるようプロパティを用意します。
この例を見てわかるとおり、比較される側であるAccountクラスでは、必ずしもIComparerを実装している必要はありません。 また、IComparerを実装する側では、比較対象に合わせて比較処理を定義することができます。
IComparable<T>
IComparable<T>インターフェイス(System名前空間)はIComparableインターフェイスのジェネリック版で、型パラメータTで指定された型との比較が可能であることを表すインターフェイスです。 IComparableと同様CompareToメソッドを実装する必要がありますが、引数の型はobjectではなく型パラメータで指定された型となるため、実装に際して型のチェックをする必要が無くなります。
以下の例は、IComparableでの例をIComparable<T>インターフェイス使ったものに書き換えたものです。
実装は省略しますが、次のコードのように型パラメータTで指定する型を変えて複数のIComparable<T>を実装することも出来ます。
IComparer<T>
IComparer<T>インターフェイス(System.Collections.Generic名前空間)はIComparerインターフェイスのジェネリック版で、型パラメータTで指定された型に特化した比較処理を提供するためのインターフェイスです。 IComparerと同様Compareメソッドを実装する必要がありますが、引数の型はobjectではなく型パラメータで指定された型となるため、実装に際して型のチェックをする必要が無くなります。
以下の例は、IComparerでの例をIComparer<T>インターフェイス使ったものに書き換えたものです。
Comparison<T>
Comparison<T>デリゲート(System名前空間)は、二つのオブジェクトを比較するメソッド、つまりIComparer<T>.Compareメソッドのような比較処理を行うメソッドを表すデリゲートです。 Array.SortおよびList<T>.Sortメソッドはこのデリゲートを引数に取ることができ、ソートの際の比較処理に使用することが出来ます。
このデリゲートを用いる利点は、IComparer<T>やIComparable<T>を実装したクラスを用意しなくても、比較処理を定義したメソッドを用意するだけでソートが行える点にあります。 このデリゲートと匿名メソッドやラムダ式を組み合わせて用いることでより簡単に比較処理を記述することも出来ます。
Comparer<T>
Comparer<T>クラス(System.Collections.Generic名前空間)は、IComparer非ジェネリックインターフェイスとIComparer<T>ジェネリックインターフェイスを実装する抽象クラスです。 このクラスの機能と存在意義はIComparer<T>と同じですが、IComparerとIComparer<T>の二つのインターフェイスを実装するクラスを作成しなくても、このクラスを継承して抽象メソッドであるCompareメソッドを実装するだけで同じ機能を提供することができるようになります。
ジェネリックな比較処理を提供することを考えたとき、ArrayList.Sortなど非ジェネリックなコレクションのソートでも動作するようにしたい場合は、IComparer<T>だけでなくIComparerも実装する必要があります。 Comparer<T>クラスでは引数の型のチェックなどは既に実装されているため、簡単にジェネリック・非ジェネリック両方の比較処理を提供できるようになります。
以下の例では、Comparer<T>を継承したクラスを作成し、ArrayList.Sortでのソートで使用しています。 ソートしようとしているArrayListの要素にnull(Nothing)が含まれている点に注目してください。
なお、Comparer<T>クラスで実装されているIComparer.Compareの比較処理では、null(Nothing)はnull以外のどのような値よりも小さいとみなされます。 この動作を変えたい場合は、派生クラスでIComparer.Compareを再実装する必要があります。
Comparer.Create
.NET Framework 4.5からは、任意のComparison<T>デリゲートからComparer<T>クラスのインスタンスを作成するメソッドComparer<T>.Createが追加されました。 このメソッドを使うことにより、Comparer<T>を継承したクラスを作成しなくても、Comparison<T>デリゲートをIComparer<T>として使用することができるようになります。
このメソッドは、IComparer<T>インターフェイスはサポートしているがComparison<T>デリゲートはサポートしていないクラスやメソッドを使用する際に特に便利です。 例えばSortedListでは、コンストラクタにIComparer<T>を指定することで要素のソート順を変更することができますが、Comparison<T>デリゲートを直接指定することはできません。 そのため、既存の比較用メソッドをSortedListで使用しようとした場合、メソッドをComparer<T>を継承(もしくはIComparer<T>を実装)したクラスに書き換える必要がありますが、Comparer<T>.Createメソッドを使えばコードを一切変更することなくそのまま活用することができます。
次の例では、int型の値を降順で比較するメソッドからComparer<int>を作成し、SortedListの並び替え順として使用するようにしています。 これにより、SortedListの内容はキーを降順にした並べ替えられます。
StringComparer
StringComparerクラス(System名前空間)は、文字列比較に特化したIComparerの実装です。 大文字小文字を無視した比較、現在のカルチャ・インバリアントカルチャの規則に基づいた比較などを行う実装が用意されています。 詳細については、StringComparerについての個別の解説を参照してください。