うら干物書き

ゲームを作りたい!

【C#,LINQ】ThenBy,ThenByDescending~並べ替えたものを更に並べ替えたいとき~

 C#のLINQの関数であるThenBy()ThenByDescending()の使い方についてです。
 すでに並べ替えた後のシーケンスに対して、更に追加の条件で並べ替えることが出来ます。
f:id:urahimono:20180721063203p:plain


この記事には.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,],

比較処理を自作する

 では逆にすでに並べ替え方式が決まっているintstringなど既存の型に関して、比較処理を自作してみましょう。
 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のリンク