C#のLINQの関数であるThenBy()
、ThenByDescending()
の使い方についてです。
すでに並べ替えた後のシーケンスに対して、更に追加の条件で並べ替えることが出来ます。
この記事には.NET Framework 4.6.1を使用しています。
並べ替えたものを更に並べ替える
OrderBy()
、OrderByDescending()
で配列やリストを並べ替えることができます。
www.urablog.xyz
ただ、OrderBy()
などで指定した条件だけでは、序列が同じ要素ができてしまい、第二キーとして、更に並べ替えの条件を追加したいときが出てくると思います。
そんな時には、ThenBy()
、ThenByDescending()
が使えますよ。
ただ注意点として、ThenBy()
、ThenByDescending()
は、OrderBy()
、OrderByDescending()
を使用した際に返されるIOrderedEnumerable
型に対して使用する関数です。
IOrderedEnumerable
https://msdn.microsoft.com/ja-jp/library/bb534852.aspx
そのため、ほかのLINQの関数と同じように、配列やリストに対して直接使用するとエラーになってしまうので気を付けてくださいね。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { static void Main( string[] args ) { // フルーツの名前データ(MSDNのサンプルそのまま持ってきたよ)。 string[] fruits = { "grape", "passionfruit", "banana", "mango", "orange", "raspberry", "apple", "blueberry" }; // えらー! // 配列はIOrderedEnumerable<T>じゃないから、ThenBy()は使えないよ。 fruits.ThenBy( value => value ); // 入力待ち用 System.Console.ReadKey(); } } // class Program
正しい使い方は以下の通りです。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { static void Main( string[] args ) { // フルーツの名前データ(MSDNのサンプルそのまま持ってきたよ)。 string[] fruits = { "grape", "passionfruit", "banana", "mango", "orange", "raspberry", "apple", "blueberry" }; // OrderBy()で返される値に対してなら大丈夫! IOrderedEnumerable<string> orderedFruits = fruits.OrderBy( value => value ); orderedFruits.ThenBy( value => value ); // 入力待ち用 System.Console.ReadKey(); } } // class Program
昇順に
ThenBy()
を使えば、昇順に追加の条件で並べ替えることができます。
public static IOrderedEnumerable<TSource> ThenBy<TSource, TKey>( this IOrderedEnumerable<TSource> source, Func<TSource, TKey> keySelector );
Enumerable.ThenBy(TSource, TKey) メソッド (IOrderedEnumerable(TSource), Func(TSource, TKey)) (System.Linq)
第一引数に並べ替える要素のプロパティを取得する処理を記述します。
要素そのもので並べ返る場合は自分自身を返せばOKです。
以下の例ではラムダ式を用いて、要素自身を返しています。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { static void Main( string[] args ) { // フルーツの名前データ(MSDNのサンプルそのまま持ってきたよ)。 string[] fruits = { "grape", "passionfruit", "banana", "mango", "orange", "raspberry", "apple", "blueberry" }; // 文字列の長さを昇順で並べ替え、 IOrderedEnumerable<string> orderByFruits = fruits.OrderBy( value => value.Length ); // 同じ長さのものはアルファベットの昇順で並べ替える。 IOrderedEnumerable<string> thenByFruits = orderByFruits.ThenBy( value => value ); System.Console.WriteLine("fruits :{0}", fruits.Text()); System.Console.WriteLine("orderBy :{0}", orderByFruits.Text()); System.Console.WriteLine("thenBy :{0}", thenByFruits.Text()); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
fruits :[grape], [passionfruit], [banana], [mango], [orange], [raspberry], [apple], [blueberry],
orderBy :[grape], [mango], [apple], [banana], [orange], [raspberry], [blueberry], [passionfruit],
thenBy :[apple], [grape], [mango], [banana], [orange], [blueberry], [raspberry], [passionfruit],
上記の例では、文字列の長さが同じものに対しては、アルファベット順に並ぶように指定しています。
「grape」、「mango」、「apple」の並びが、
「apple」、「grape」、「mango」に変わったのが確認できます。
降順に
降順にする場合は、ThenByDescending()
が使えます。
public static IOrderedEnumerable<TSource> ThenByDescending<TSource, TKey>( this IOrderedEnumerable<TSource> source, Func<TSource, TKey> keySelector );
Enumerable.ThenByDescending(TSource, TKey) メソッド (IOrderedEnumerable(TSource), Func(TSource, TKey)) (System.Linq)
使用方法は、ThenBy()
と同じです。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { static void Main( string[] args ) { // フルーツの名前データ(MSDNのサンプルそのまま持ってきたよ)。 string[] fruits = { "grape", "passionfruit", "banana", "mango", "orange", "raspberry", "apple", "blueberry" }; // 文字列の長さを昇順で並べ替え、 IOrderedEnumerable<string> orderByFruits = fruits.OrderBy( value => value.Length ); // 同じ長さのものはアルファベットの降順で並べ替える。 IOrderedEnumerable<string> thenByFruits = orderByFruits.ThenByDescending( value => value ); System.Console.WriteLine("fruits :{0}", fruits.Text()); System.Console.WriteLine("orderBy :{0}", orderByFruits.Text()); System.Console.WriteLine("thenBy :{0}", thenByFruits.Text()); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
fruits :[grape], [passionfruit], [banana], [mango], [orange], [raspberry], [apple], [blueberry],
orderBy :[grape], [mango], [apple], [banana], [orange], [raspberry], [blueberry], [passionfruit],
thenBy :[mango], [grape], [apple], [orange], [banana], [raspberry], [blueberry], [passionfruit],
先ほどとは違い、
「grape」、「mango」、「apple」の並びが、
「mango」、「grape」、「apple」とアルファベットの降順になっていることが確認できます。
なおかつOrderBy()
を使って、文字列の長さを昇順で並べているところは、きちんと維持されていることも確認できます。
更に更に並べ替える
ThenBy()
、ThenByDescending()
で並べかえたシーケンスに対して、更にThenBy()
、ThenByDescending()
を使うこともできます。
こんなクラスを用意してみました。
Program.cs
private class Parameter { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2},", ID, Age, Name ); } }
このクラスの配列に対して、以下のように、
- IDの昇順
- 年齢の昇順
- アルファベット降順
の順で並べ替えるような処理も作成することができます。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class Parameter { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2},", ID, Age, Name ); } } static void Main( string[] args ) { // 人物データ Parameter[] persons = { new Parameter() { ID = 0, Age = 30, Name = "A" }, new Parameter() { ID = 1, Age = 25, Name = "B" }, new Parameter() { ID = 2, Age = 18, Name = "C" }, new Parameter() { ID = 1, Age = 30, Name = "D" }, new Parameter() { ID = 1, Age = 25, Name = "E" }, new Parameter() { ID = 2, Age = 15, Name = "F" }, }; // IDの昇順で並べ替え、 IOrderedEnumerable<Parameter> orderByID = persons.OrderBy( value => value.ID ); // 同じIDの場合は、年齢の昇順で並べ替える。 IOrderedEnumerable<Parameter> thenByAge = orderByID.ThenBy( value => value.Age ); // それでも一緒なら、アルファベットの降順で並べ替える。 IOrderedEnumerable<Parameter> thenByName = orderByID.ThenByDescending( value => value.Name ); System.Console.WriteLine("persons :{0}", persons.Text()); System.Console.WriteLine("orderByID :{0}", orderByID.Text()); System.Console.WriteLine("thenByAge :{0}", thenByAge.Text()); System.Console.WriteLine("thenByName :{0}", thenByName.Text()); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
persons :[ID:0, Age:30, Name:A,], [ID:1, Age:25, Name:B,], [ID:2, Age:18, Name:C,], [ID:1, Age:30, Name:D,], [ID:1, Age:25, Name:E,], [ID:2, Age:15, Name:F,],
orderByID :[ID:0, Age:30, Name:A,], [ID:1, Age:25, Name:B,], [ID:1, Age:30, Name:D,], [ID:1, Age:25, Name:E,], [ID:2, Age:18, Name:C,], [ID:2, Age:15, Name:F,],
thenByAge :[ID:0, Age:30, Name:A,], [ID:1, Age:25, Name:B,], [ID:1, Age:25, Name:E,], [ID:1, Age:30, Name:D,], [ID:2, Age:15, Name:F,], [ID:2, Age:18, Name:C,],
thenByName :[ID:0, Age:30, Name:A,], [ID:1, Age:25, Name:E,], [ID:1, Age:30, Name:D,], [ID:1, Age:25, Name:B,], [ID:2, Age:15, Name:F,], [ID:2, Age:18, Name:C,],
今回はいちいち、変数に設定してから、ThenBy()
、ThenByDescending()
を使うようにしていますが、直接ThenBy()
などをつなげて書くことだってできます。
並べ替える際の比較について
さて、OrderBy()
の時も書いた内容ではあるのですが、IComparable
を実装していないクラスなどを直接並べ替えの条件に使用してしまうと例外が発生します。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class Parameter { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2},", ID, Age, Name ); } } static void Main( string[] args ) { // 人物データ Parameter[] persons = { new Parameter() { ID = 0, Age = 30, Name = "A" }, new Parameter() { ID = 1, Age = 25, Name = "B" }, new Parameter() { ID = 2, Age = 18, Name = "C" }, new Parameter() { ID = 1, Age = 30, Name = "D" }, new Parameter() { ID = 1, Age = 25, Name = "E" }, new Parameter() { ID = 2, Age = 15, Name = "F" }, }; // IDの昇順で並べ替え、 IOrderedEnumerable<Parameter> orderByID = persons.OrderBy( value => value.ID ); // 人物情報クラスそのものを昇順に並べる。 IOrderedEnumerable<Parameter> thenByParam = orderByID.ThenBy( value => value ); try { // そして使う。 // きっと例外が発生する(遅延実行のため、ThenBy()を使った直後には例外は発生しない)。 thenByParam.Any(); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } System.Console.WriteLine("persons :{0}", persons.Text()); System.Console.WriteLine("orderByID :{0}", orderByID.Text()); System.Console.WriteLine("thenByParam :{0}", thenByParam.Text()); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
System.ArgumentException: 少なくとも1つのオブジェクトで IComparable を実装しなければなりません。
どちらが先か後かが判断できませんからね。
IComparable
を継承することで、直接並べ替える要素として使うことができるようになります。
IComparable(T) インターフェイス (System)
Program.cs
private class Parameter : System.IComparable<Parameter> { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } // IComparableインターフェース用の比較関数 // 返り値が 負数の場合は 自分 の方が 前 // 返り値が 正数の場合は 自分 の方が 後 // 返り値が 0 の場合は 自分 と 相手 が同じ public int CompareTo( Parameter i_other ) { if( i_other == null ) { return -1; } int compareID = ID - i_other.ID; if( compareID != 0 ) { // IDが小さい方が前。 return compareID; } int compareAge = Age - i_other.Age; if( compareAge != 0 ) { // Ageが小さい方が前。 return compareAge; } // 小さい文字コードが前。 return string.Compare( Name, i_other.Name ); } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2},", ID, Age, Name ); } }
こんな感じですね。
これならば先ほどの書き方でも例外は発生しません。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class Parameter : System.IComparable<Parameter> { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } // IComparableインターフェース用の比較関数 // 返り値が 負数の場合は 自分 の方が 前 // 返り値が 正数の場合は 自分 の方が 後 // 返り値が 0 の場合は 自分 と 相手 が同じ public int CompareTo( Parameter i_other ) { if( i_other == null ) { return -1; } int compareID = ID - i_other.ID; if( compareID != 0 ) { // IDが小さい方が前。 return compareID; } int compareAge = Age - i_other.Age; if( compareAge != 0 ) { // Ageが小さい方が前。 return compareAge; } // 小さい文字コードが前。 return string.Compare( Name, i_other.Name ); } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2},", ID, Age, Name ); } } static void Main( string[] args ) { // 人物データ Parameter[] persons = { new Parameter() { ID = 0, Age = 30, Name = "A" }, new Parameter() { ID = 1, Age = 25, Name = "B" }, new Parameter() { ID = 2, Age = 18, Name = "C" }, new Parameter() { ID = 1, Age = 30, Name = "D" }, new Parameter() { ID = 1, Age = 25, Name = "E" }, new Parameter() { ID = 2, Age = 15, Name = "F" }, }; // IDの昇順で並べ替え、 IOrderedEnumerable<Parameter> orderByID = persons.OrderBy( value => value.ID ); // 人物情報クラスそのものを昇順に並べる。 // (Parameterクラスの比較では各要素の比較をしてるから、OrderBy()で片がついちゃんだけどね……) IOrderedEnumerable<Parameter> thenByParam = orderByID.ThenBy( value => value ); System.Console.WriteLine("persons :{0}", persons.Text()); System.Console.WriteLine("orderByID :{0}", orderByID.Text()); System.Console.WriteLine("thenByParam :{0}", thenByParam.Text()); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
persons :[ID:0, Age:30, Name:A,], [ID:1, Age:25, Name:B,], [ID:2, Age:18, Name:C,], [ID:1, Age:30, Name:D,], [ID:1, Age:25, Name:E,], [ID:2, Age:15, Name:F,],
orderByID :[ID:0, Age:30, Name:A,], [ID:1, Age:25, Name:B,], [ID:1, Age:30, Name:D,], [ID:1, Age:25, Name:E,], [ID:2, Age:18, Name:C,], [ID:2, Age:15, Name:F,],
thenByParam :[ID:0, Age:30, Name:A,], [ID:1, Age:25, Name:B,], [ID:1, Age:25, Name:E,], [ID:1, Age:30, Name:D,], [ID:2, Age:15, Name:F,], [ID:2, Age:18, Name:C,],
比較処理を自作する
では逆にすでに並べ替え方式が決まっているint
やstring
など既存の型に関して、比較処理を自作してみましょう。
IComparer
を検証したクラスをThenBy()
、ThenByDescending()
に指定してあげればOKです。
public static IOrderedEnumerable<TSource> ThenBy<TSource, TKey>( this IOrderedEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer );
Enumerable.ThenBy(TSource, TKey) メソッド (IOrderedEnumerable(TSource), Func(TSource, TKey), IComparer(TKey)) (System.Linq)
public static IOrderedEnumerable<TSource> ThenByDescending<TSource, TKey>( this IOrderedEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer );
Enumerable.ThenByDescending(TSource, TKey) メソッド (IOrderedEnumerable(TSource), Func(TSource, TKey), IComparer(TKey)) (System.Linq)
IComparer
IComparer(T) インターフェイス (System.Collections.Generic)
こんな感じに作ってみます。
Program.cs
private class CompareString : IComparer<string> { // IComparerインターフェース用の比較関数 // 返り値が 負数の場合は i_lhs の方が 前 // 返り値が 正数の場合は i_lhs の方が 後 // 返り値が 0 の場合は i_lhs と i_rhs は同じ public int Compare( string i_lhs, string i_rhs ) { bool isInitiallyG_L = !string.IsNullOrEmpty( i_lhs ) ? i_lhs.StartsWith( "g" ) : false; bool isInitiallyG_R = !string.IsNullOrEmpty( i_rhs ) ? i_rhs.StartsWith( "g" ) : false; // 頭文字が"g"のものが圧倒的にすごいのだ! if( isInitiallyG_L && !isInitiallyG_R ) { return -1; } if( !isInitiallyG_L && isInitiallyG_R ) { return 1; } // gでないやつに興味はない。 return string.Compare( i_lhs, i_rhs ); } }
これを用いれば独自の比較方式で並べ替えることができます。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class CompareString : IComparer<string> { // IComparerインターフェース用の比較関数 // 返り値が 負数の場合は i_lhs の方が 前 // 返り値が 正数の場合は i_lhs の方が 後 // 返り値が 0 の場合は i_lhs と i_rhs は同じ public int Compare( string i_lhs, string i_rhs ) { bool isInitiallyG_L = !string.IsNullOrEmpty( i_lhs ) ? i_lhs.StartsWith( "g" ) : false; bool isInitiallyG_R = !string.IsNullOrEmpty( i_rhs ) ? i_rhs.StartsWith( "g" ) : false; // 頭文字が"g"のものが圧倒的にすごいのだ! if( isInitiallyG_L && !isInitiallyG_R ) { return -1; } if( !isInitiallyG_L && isInitiallyG_R ) { return 1; } // gでないやつに興味はない。 return string.Compare( i_lhs, i_rhs ); } } static void Main( string[] args ) { // フルーツの名前データ(MSDNのサンプルそのまま持ってきたよ)。 string[] fruits = { "grape", "passionfruit", "banana", "mango", "orange", "raspberry", "apple", "blueberry" }; // 文字列の長さを昇順で並べ替え、 IOrderedEnumerable<string> orderByFruits = fruits.OrderBy( value => value.Length ); // 同じ長さのものはアルファベットの昇順で並べ替える(ただし、頭文字gはなによりも優先される)。 CompareString compare = new CompareString(); IOrderedEnumerable<string> thenByFruits = orderByFruits.ThenBy( value => value, compare); System.Console.WriteLine("fruits :{0}", fruits.Text()); System.Console.WriteLine("orderBy :{0}", orderByFruits.Text()); System.Console.WriteLine("thenBy :{0}", thenByFruits.Text()); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
fruits :[grape], [passionfruit], [banana], [mango], [orange], [raspberry], [apple], [blueberry],
orderBy :[grape], [mango], [apple], [banana], [orange], [raspberry], [blueberry], [passionfruit],
thenBy :[grape], [apple], [mango], [banana], [orange], [blueberry], [raspberry], [passionfruit],
こんな感じでThenBy()
、ThenByDescending()
を七兆回ほど使って、世界を並べ替えてみてください。
LINQのリンク
LINQ一覧
www.urablog.xyzOrderBy, OrderByDescending
シーケンス(配列やリスト)を並べ替えたい!
www.urablog.xyz