ここでは複合型を含むコレクションのソートについて見ていきます。
複合型のソート
数値や文字列などの型は、あらかじめデフォルトの並べ替え順序が定義されているため特に何も指定しなくてもArray.SortやList.Sortなどのメソッドでソートを行うことが出来ます。 一方、独自に定義したクラス・構造体などの複合型(1つ以上の基本型を組み合わせて構成される型)の場合は、並べ替え順序を個別に定義しない限りソートすることが出来ません。
仮に型が比較演算子をオーバーロードしていてもそれだけではソート出来るようにはなりません。 この点については、比較演算子のオーバーロードとIComparable<T>インターフェイスで詳しく解説しています。
ここでは、複合型をソートする方法・並べ替え順序を定義する方法について解説します。
Sort + Comparison<T>
降順でのソートでも述べているとおり、Array.SortメソッドおよびList.Sortメソッドでは、並び替え順序を定義したメソッドを用意し、それをComparison<T>デリゲートとしてSortメソッドの引数に指定することにより、デフォルト(昇順)以外の順序、独自に定義した順序でソートすることが出来ます。
早速、独自に定義したクラスをソートする方法、並べ替え順序を定義する方法について順を追って見ていきます。
ここではNameフィールド(string)とIDフィールド(int)を持つクラスAccountを作成し、Accountクラスのインスタンスが格納されたリストをID順にソートすることにします。 具体的には、次の不完全なコードでソートが出来るように実装していきます。
まずは、並べ替え順序を定義をするために、比較を行うメソッドを用意します。 このメソッドは、Comparison<T>デリゲートの形でSortメソッドに渡す必要があります。 Comparison<T>デリゲートの形式を見てみると、次のようになっています。
この形式を分かりやすく説明すると、「ある型Tの要素二つを比べて、どちらが小さいか・大きいかをintの数値で返す」というものです。 Sortメソッドの内部では、各要素の比較が行われる度にこのメソッドが呼び出され、戻り値として返される大小関係によって要素の並べ替えが行われることになります。 この形式にあわせて、Accountクラスを比較するメソッドCompareAccountのひな形を作成すると次のようになります。
次にこのメソッドを実装します。 比較を行うメソッドでは二つの要素の大小関係を戻り値として返します。 具体的には、引数として渡される二つの要素xとyについて、その大小関係が
- x > y (xがyより大きい) ならば 正の数
- x < y (xがyより小さい) ならば 負の数
- x = y (xとyが等しい) ならば 0
を返すようにします。 このルールに従ってメソッドを実装すると、次のようになります。 正の数・負の数ならなんでもよいので、ここではそれぞれ1
と-1
を返しています。
当然Accountクラスのインスタンスそのものを比較することはできないため、このままでは不十分です。 ここではID順でのソートを行いたいため、インスタンスの大小関係がIDフィールドの値によって定まるように実装します。 最終的に、ID順でソートするためのメソッドCompareAccountは次のようになります。
最後に、このメソッドをSortメソッドの引数として渡せば、リストはこのメソッドで定義された順序、すなわちID順にソートされるようになります。
名前順にソートしたければ、Nameフィールドの値を比較するようにCompareAccountメソッドの実装を変えるだけで出来ます。 なお、文字列の比較はその都度定義しなくても、既に用意されているString.Compareメソッドを使うことが出来ます。
このように、並べ替え順序(大小関係)を定義することができ、Comparison<T>デリゲートの形式でSortメソッドに渡すことができれば、どのような複合型でもソートを行うことが出来るようになります。
なお、ここまでは昇順となるようにしてきましたが、降順にしたければCompareAccountで定義する大小関係を逆にするか、Reverseメソッドを使ってソートした後で逆順にするだけです。
降順でのソートについては基本型のソートと昇順・降順でのソート §.降順でのソートを参照してください。
文字列の比較やString.Compareメソッドの詳細に関しては文字列の探索・比較 §.文字列の比較・等価性の検証を参照してください。
null/Nothingの考慮
ソートしたい対象が参照型のコレクションで、かつコレクションにnull
/Nothing
が含まれる可能性がある場合は、比較を行うメソッドにおいてもnull
/Nothing
だった場合の考慮を行う必要があります。 具体的には次のようにnull
/Nothing
との比較を行うコードを追加します。
.NET Frameworkにおける文字列型(Stringクラス)では、null/Nothingは他のいずれの値よりも小さい、またnull/Nothing同士は等しいものとして比較が行われるため、上記の例もこれに準じた結果を返すようにしています。
以下は上記のように定義した比較メソッドを使ってnull
/Nothing
を含むコレクションをソートする例です。
このように、昇順の場合はnull
/Nothing
が前の方へ並べ替えられます。 後ろの方へ並べ替えたい場合は、比較メソッドを変更してnull/Nothingは他のいずれの値よりも大きいと定義することによって実現できます。
OrderBy
複合型は1つ以上の基本型から構成されるため、複合型のソートで実際にキーとして選ばれるのは単純型のメンバとなります。 また、単純型のソートの場合でも、単に昇順・降順でのソートで十分な場合がほとんどで、独自の並び替え順序が必要になる事はそう多くありません。
OrderByメソッドは、複合型をソートしたい場合でもキーとなるメンバを選択するだけでソートすることが出来、また選択したキーが単純型であれば並べ替え順序を定義する必要もないため、より少ない記述でソートを行うことが出来ます。
OrderByメソッドの詳細とキーの選択については基本型のソートと昇順・降順でのソート §.OrderByメソッドとキーの選択で解説しているので省略しますが、先に挙げた例をList.SortではなくOrderByメソッドでソートするように変えると、以下のようになります。
OrderByメソッドの代わりにOrderByDescendingメソッドを使うことにより、降順でソートすることが出来ます。
null/Nothingの考慮
OrderByメソッドではキーの選択を行ってソートを行います。 コレクション内の要素がnull
/Nothing
だった場合にはキーとして使う値を参照しようとしてもヌル参照となるため、ソートを継続することができません。 そのため、ソートしたい対象が参照型のコレクションで、かつコレクションにnull
/Nothing
が含まれる可能性がある場合は、要素がnull
/Nothing
だった場合の考慮を行う必要があります。
具体的には、次のようにして非nullの要素のみをソートするようにします。
- コレクション内の要素のうち、nullの要素とそうでないものを分ける (Whereメソッド)
- 非nullの要素のみソートを行う (OrderByメソッド)
- nullの要素と、ソートした非nullの要素を結合する (Concatメソッド)
以下は上記のようにしてnull
/Nothing
を含むコレクションをソートする例です。
この例ではnull
/Nothing
を先に結合しているため、null
/Nothing
が前の方へ並べ替えられます。 後ろの方へ並べ替えたい場合は、次のように結合順序を逆にすることで実現できます。
複数キーでのソート
ここでは複数のキーでソートする場合についてを扱います。 たとえば、次のようなデータがあったときに、まずAge(年齢)の若い順に並べ替え、Ageが同じ場合はIDが小さい順に並べ替えたいといった場合です。 言い換えると、Ageを第一のキー、IDを第二のキーとしてソートするための方法です。
ID | Name | Age |
3 | Bob | 16 |
1 | Dave | 25 |
2 | Alice | 16 |
5 | Charlie | 9 |
4 | Eve | 16 |
6 | Romeo | 9 |
7 | Juliet | 25 |
→ソート→
ID | Name | Age |
5 | Charlie | 9 |
6 | Romeo | 9 |
2 | Alice | 16 |
3 | Bob | 16 |
4 | Eve | 16 |
1 | Dave | 25 |
7 | Juliet | 25 |
Sort + Comparison<T>
これまでの例では、並べ替え順序を定義する際に単一のキーのみを用いていましたが、並べ替え順序の定義を拡張することで複数のキーでソートできるようになります。 複数のキーでソートするには、まず第一のキーで比較し、同じだった場合は第二のキーで比較するように並べ替え順序を定義することで実現できます。
まず、一つのキーでソートする場合について振り返ると、比較を行うメソッドでは引数として渡される二つの要素xとyについて、その大小関係に従って次のような値を返すように実装する必要がありました。
- x > y ならば 正の数
- x < y ならば 負の数
- x = y ならば 0
二つのキーでソートする場合は、要素xとyについてまず第一のキーで比較を行い、第一のキーが同じだった場合は第二のキーで比較するようにします。 つまり、上記の条件式を拡張して、比較を行うメソッドでは次のような値を返すように実装します。
- 第一のキーについて x > y (x.Key1 > y.Key1) ならば 正の数
- 第一のキーについて x < y (x.Key1 < y.Key1) ならば 負の数
- 第一のキーについて x = y (x.Key1 = y.Key1) ならば さらに第二のキーで比較する
- 第二のキーについて x > y (x.Key2 > y.Key2) ならば 正の数
- 第二のキーについて x < y (x.Key2 < y.Key2) ならば 負の数
- 第二のキーについて x < y (x.Key2 = y.Key2) ならば 0
このようにすることで、各要素はまず第一のキーの順に並べ替えられ、第一のキーが同じ要素については第二のキーの順に並べ替えられるようになり、二つのキーでソートされることになります。
具体例を見ていきます。 次の例では、ID(int)、名前(string)、年齢(int)のフィールドを持つAccountクラスとそれを格納するリストを作成し、ソートしています。 ここでは第一のキーを年齢、第二のキーをIDとし、年齢が若い順、年齢が同じ場合はIDが小さい順にソートしています。
次に上記の例を少し変えて、名前を第一のキー、IDを第二のキーとしてソートしてみます。 文字列の比較にはString.Compareメソッドが使えます。 このメソッドも引数の大小関係に応じて正の数・負の数・0のいずれかが返されます。 同じ場合には0となるので、戻り値が0の場合は第二のキーで比較するようにします。
ここまではキーが二つの場合を例示しましたが、さらに多くのキーでソートしたければ、第二のキーで比較して同じなら第三のキー、第三のキーで比較して同じなら第四のキーと、比較処理を次々拡張することで実現できます。
OrderBy + ThenBy
OrderByメソッドのみを使ったソートでは単一のキーでしかソート出来ませんが、ThenByメソッドと組み合わせて使うと、複数のキーでのソートが出来るようになります。
ThenByメソッドは、OrderByメソッドの戻り値であるIOrderedEnumerable<TSource>に対する拡張メソッドであり、OrderByメソッドの結果(単一のキーでソートした結果)に対してThenByメソッドを呼び出すことにより、第二のキーでソートした結果を得ることが出来ます。
早速、具体例を見ていきます。 次の例では、ID(int)、名前(string)、年齢(int)のフィールドを持つAccountクラスとそれを格納するリストを作成し、ソートしています。 ここでは第一のキーを年齢、第二のキーをIDとし、年齢が若い順、年齢が同じ場合はIDが小さい順にソートしています。
さらに、ThenByメソッドの戻り値もIOrderedEnumerable<TSource>であるため、ThenByメソッドの戻り値に対してThenByメソッドを呼び出すと、第三のキーでソートした結果を得られるようになります。 このように、キーの数に応じてThenByメソッドを続けて呼び出すことで複数キーでのソートを行うことが出来ます。
OrderByメソッドと同様、ThenByメソッドも遅延実行されるため、戻り値に対してforeach等で列挙操作を行うことで初めてソートが行われる点に注意してください。
ソートと遅延実行に関する注意点については基本型のソートと昇順・降順でのソート §.元のコレクションに対する変更とOrderByメソッドの結果への影響を参照してください。
パフォーマンスの比較
ここではソートを行ういくつかのケースについてパフォーマンスの比較を行った結果を紹介します。 比較メソッドの定義方法やソートするデータの種類等によっては結果が変わるので、ここで紹介している結果はあくまで参考程度のものです。
Sort vs OrderBy
複数のキーでソートするのに、Sort(Comparison<T>)メソッドを使った場合とOrderByメソッド+ThenByメソッドを使った場合を比較すると、速度の点では大きな差は無いようです。 以下のコードでは、先と同様100個、10,000個、1,000,000個の要素を持つList<Tuple<int, int>>に対してSort(Comparison<T>)メソッドとOrderByメソッド+ThenByメソッドでソートし、列挙する操作をそれぞれ3回試行した場合の経過時間を比較しています。
単一のキーでソートした場合の結果については、基本型のソートと昇順・降順でのソート §.パフォーマンスの比較を参照してください。