徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【C#,LINQ】OrderBy,OrderByDescending~配列やリストを並べ替えたいとき~

 C#のLINQの関数であるOrderBy()OrderByDescending()の使い方についてです。
 配列やリストの要素の順番を並べ替えることが出来ます。
f:id:urahimono:20180721062027p:plain


この記事には.NET Framework 4.6.1を使用しています。

並べ替える

 配列やリスト内の要素を昇順や降順に並べ替えるのは面倒くさいですよね。
 そんなときはLINQOrderBy()OrderByDescending()が便利です。

昇順に

 OrderBy()を使えば昇順に並べ替えられます。

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector );
Enumerable.OrderBy(TSource, TKey) メソッド (IEnumerable(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 )
    {
        int[]       dataA   = new int[] { 3, 1, 2, 0, 4 };
        List<float> dataB   = new List<float>() { 1.5f, 1.3f, 3.2f };
        string[]    dataC   = new string[] { "正一郎", "清次郎", "誠三郎", "征史郎" };

        // 昇順に並べ替える
        IOrderedEnumerable<int>     orderedDataA    = dataA.OrderBy( value => value );
        IOrderedEnumerable<float>   orderedDataB    = dataB.OrderBy( value => value );
        IOrderedEnumerable<string>  orderedDataC    = dataC.OrderBy( value => value );

        System.Console.WriteLine( "dataA        :{0}", dataA.Text() );
        System.Console.WriteLine( "dataA ordered:{0}", orderedDataA.Text() );
        System.Console.WriteLine( "dataB        :{0}", dataB.Text() );
        System.Console.WriteLine( "dataB ordered:{0}", orderedDataB.Text() );
        System.Console.WriteLine( "dataC        :{0}", dataC.Text() );
        System.Console.WriteLine( "dataC ordered:{0}", orderedDataC.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

dataA :[3], [1], [2], [0], [4],
dataA ordered:[0], [1], [2], [3], [4],
dataB :[1.5], [1.3], [3.2],
dataB ordered:[1.3], [1.5], [3.2],
dataC :[正一郎], [清次郎], [誠三郎], [征史郎],
dataC ordered:[征史郎], [正一郎], [清次郎], [誠三郎],

 次の例では、要素の特定のプロパティで並べ替える処理を記述しています。

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 override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 3, Name = "清次郎" },
            new Parameter() { ID = 2, Name = "誠三郎" },
            new Parameter() { ID = 5, Name = "征史郎" },
        };


        // IDの昇順に並べ替える
        IOrderedEnumerable<Parameter>   orderedParameters   = parameters.OrderBy( value => value.ID );

        System.Console.WriteLine( "parameters        :{0}", parameters.Text() );
        System.Console.WriteLine( "parameters ordered:{0}", orderedParameters.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

parameters :[ID:0, Name:正一郎], [ID:3, Name:清次郎], [ID:2, Name:誠三郎], [ID:5, Name:征史郎],
parameters ordered:[ID:0, Name:正一郎], [ID:2, Name:誠三郎], [ID:3, Name:清次郎], [ID:5, Name:征史郎],

降順に

 OrderByDescending()を使えば降順に並べ替えられます。

public static IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector );
Enumerable.OrderByDescending(TSource, TKey) メソッド (IEnumerable(TSource), Func(TSource, TKey)) (System.Linq)

 使い方はOrderBy()と同じです。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    static void Main( string[] args )
    {
        int[]       dataA   = new int[] { 3, 1, 2, 0, 4 };
        List<float> dataB   = new List<float>() { 1.5f, 1.3f, 3.2f };
        string[]    dataC   = new string[] { "正一郎", "清次郎", "誠三郎", "征史郎" };

        // 降順に並べ替える
        IOrderedEnumerable<int>     orderedDataA    = dataA.OrderByDescending( value => value );
        IOrderedEnumerable<float>   orderedDataB    = dataB.OrderByDescending( value => value );
        IOrderedEnumerable<string>  orderedDataC    = dataC.OrderByDescending( value => value );

        System.Console.WriteLine( "dataA        :{0}", dataA.Text() );
        System.Console.WriteLine( "dataA ordered:{0}", orderedDataA.Text() );
        System.Console.WriteLine( "dataB        :{0}", dataB.Text() );
        System.Console.WriteLine( "dataB ordered:{0}", orderedDataB.Text() );
        System.Console.WriteLine( "dataC        :{0}", dataC.Text() );
        System.Console.WriteLine( "dataC ordered:{0}", orderedDataC.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

dataA :[3], [1], [2], [0], [4],
dataA ordered:[4], [3], [2], [1], [0],
dataB :[1.5], [1.3], [3.2],
dataB ordered:[3.2], [1.5], [1.3],
dataC :[正一郎], [清次郎], [誠三郎], [征史郎],
dataC ordered:[誠三郎], [清次郎], [正一郎], [征史郎],

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 override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 3, Name = "清次郎" },
            new Parameter() { ID = 2, Name = "誠三郎" },
            new Parameter() { ID = 5, Name = "征史郎" },
        };


        // IDの降順に並べ替える
        IOrderedEnumerable<Parameter>   orderedParameters   = parameters.OrderByDescending( value => value.ID );

        System.Console.WriteLine( "parameters        :{0}", parameters.Text() );
        System.Console.WriteLine( "parameters ordered:{0}", orderedParameters.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

parameters :[ID:0, Name:正一郎], [ID:3, Name:清次郎], [ID:2, Name:誠三郎], [ID:5, Name:征史郎],
parameters ordered:[ID:5, Name:征史郎], [ID:3, Name:清次郎], [ID:2, Name:誠三郎], [ID:0, Name:正一郎],

並べ替える際にどうやって比較しているの

 さて、OrderBy()OrderByDescending()を使ってきました。
 ここで気になることがあります。
 並べ替えているということは、要素同士を比較してどちらが前か後ろかを判定しているということになります。
 intstringなど既存の型ならいいのですが、自分で作ったクラスの場合は、うまく比較が出来るのでしょうか。
 クラス自身をOrderBy()で並べ替える処理を作成してみましょう。

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 override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 3, Name = "清次郎" },
            new Parameter() { ID = 2, Name = "誠三郎" },
            new Parameter() { ID = 5, Name = "征史郎" },
        };


        // クラスそのものを昇順に並べ替える
        IOrderedEnumerable<Parameter>   orderedParameters   = parameters.OrderBy( value => value );

        try
        {
            // 並べ替えた結果を見てみよう。
            bool isAny = orderedParameters.Any();
        }
        catch( System.Exception i_exception )
        {
            System.Console.WriteLine( "{0}", i_exception );
            System.Console.ReadKey();
            return;
        }


        System.Console.WriteLine( "parameters        :{0}", parameters.Text() );
        System.Console.WriteLine( "parameters ordered:{0}", orderedParameters.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; }

    // IComparableインターフェース用の比較関数
    // 返り値が 負数の場合は 自分 の方が 前
    // 返り値が 正数の場合は 自分 の方が 後
    // 返り値が 0 の場合は 自分 と 相手 が同じ
    public int CompareTo( Parameter i_other )
    {
        if( i_other == null )
        {
            return -1;
        }

        // IDが小さい方が前。
        return ID - i_other.ID;
    }

    public override string ToString()
    {
        return string.Format( "ID:{0}, Name:{1}", ID, 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; }

        // IComparableインターフェース用の比較関数
        // 返り値が 負数の場合は 自分 の方が 前
        // 返り値が 正数の場合は 自分 の方が 後
        // 返り値が 0 の場合は 自分 と 相手 が同じ
        public int CompareTo( Parameter i_other )
        {
            if( i_other == null )
            {
                return -1;
            }

            // IDが小さい方が前。
            return ID - i_other.ID;
        }

        public override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 3, Name = "清次郎" },
            new Parameter() { ID = 2, Name = "誠三郎" },
            new Parameter() { ID = 5, Name = "征史郎" },
        };

        // クラスそのものを昇順・降順に並べ替える
        IOrderedEnumerable<Parameter>   ascendingParameters     = parameters.OrderBy( value => value );
        IOrderedEnumerable<Parameter>   descendingParameters    = parameters.OrderByDescending( value => value );


        System.Console.WriteLine( "parameters     :{0}", parameters.Text() );
        System.Console.WriteLine( "parameters 昇順:{0}", ascendingParameters.Text() );
        System.Console.WriteLine( "parameters 降順:{0}", descendingParameters.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

parameters :[ID:0, Name:正一郎], [ID:3, Name:清次郎], [ID:2, Name:誠三郎], [
ID:5, Name:征史郎],
parameters 昇順:[ID:0, Name:正一郎], [ID:2, Name:誠三郎], [ID:3, Name:清次郎], [
ID:5, Name:征史郎],
parameters 降順:[ID:5, Name:征史郎], [ID:3, Name:清次郎], [ID:2, Name:誠三郎], [
ID:0, Name:正一郎],

 これなら自作クラスを自分の好みの形で並べ替えられますね。

比較処理を自作する

 さて、先ほど自作クラスにIComparableを組み込むことで、並べ替えの比較処理を自分の望む形にすることができました。
 そうなってくると、逆にintstringなど既存の型に関しては、特殊な並べ方がしたい場合にどのような対応をすればよいのでしょうか。
 そういう場合は、比較用の処理をOrderBy()OrderByDescending()に指定することが出来ます。

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer );
Enumerable.OrderBy(TSource, TKey) メソッド (IEnumerable(TSource), Func(TSource, TKey), IComparer(TKey)) (System.Linq)

public static IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer );
Enumerable.OrderByDescending(TSource, TKey) メソッド (IEnumerable(TSource), Func(TSource, TKey), IComparer(TKey)) (System.Linq)

 比較用の処理にはIComparerを継承したクラスを使用すれば大丈夫です。

IComparer
IComparer(T) インターフェイス (System.Collections.Generic)

 以下の例ではint型にて昇順に並べる場合は偶数の数値が前になるような比較処理を作成しています。

Program.cs

private class CompareInt : IComparer<int>
{
    // IComparerインターフェース用の比較関数
    // 返り値が 負数の場合は i_lhs の方が 前
    // 返り値が 正数の場合は i_lhs の方が 後
    // 返り値が 0 の場合は i_lhs と i_rhs は同じ
    public int Compare( int i_lhs, int i_rhs )
    {
        bool isEvenNumberL  = i_lhs % 2 == 0;
        bool isEvenNumberR  = i_rhs % 2 == 0;

        // 偶数の方が 前!
        if( isEvenNumberL && !isEvenNumberR )
        {
            return -1;
        }
        if( !isEvenNumberL && isEvenNumberR )
        {
            return 1;
        }

        // 両方とも偶数または奇数の場合は、数値の低い方が前!
        return i_lhs - i_rhs;
    }
}

 これを使用してみましょう。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    private class CompareInt : IComparer<int>
    {
        // IComparerインターフェース用の比較関数
        // 返り値が 負数の場合は i_lhs の方が 前
        // 返り値が 正数の場合は i_lhs の方が 後
        // 返り値が 0 の場合は i_lhs と i_rhs は同じ
        public int Compare( int i_lhs, int i_rhs )
        {
            bool isEvenNumberL  = i_lhs % 2 == 0;
            bool isEvenNumberR  = i_rhs % 2 == 0;

            // 偶数の方が 前!
            if( isEvenNumberL && !isEvenNumberR )
            {
                return -1;
            }
            if( !isEvenNumberL && isEvenNumberR )
            {
                return 1;
            }

            // 両方とも偶数または奇数の場合は、数値の低い方が前!
            return i_lhs - i_rhs;
        }
    }

    static void Main( string[] args )
    {
        int[]   numbers = new int[] { 3, 1, 2, 0, 4, 4, 7, 4, 8, 9 };

        // 自作の比較を用いて、昇順・降順に並べ替える
        CompareInt compare = new CompareInt();
        IOrderedEnumerable<int> ascendingNumbers    = numbers.OrderBy( value => value, compare );
        IOrderedEnumerable<int> descendingNumbers   = numbers.OrderByDescending( value => value, compare );


        System.Console.WriteLine( "numbers     :{0}", numbers.Text() );
        System.Console.WriteLine( "numbers 昇順:{0}", ascendingNumbers.Text() );
        System.Console.WriteLine( "numbers 降順:{0}", descendingNumbers.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

numbers :[3], [1], [2], [0], [4], [4], [7], [4], [8], [9],
numbers 昇順:[0], [2], [4], [4], [4], [8], [1], [3], [7], [9],
numbers 降順:[9], [7], [3], [1], [8], [4], [4], [4], [2], [0],

 こんな感じでOrderBy()OrderByDescending()を七兆回ほど使って、世界を並べ替えてみてください。

LINQのリンク

  • LINQ一覧
    www.urablog.xyz

  • ThenBy, ThenByDescending
     シーケンス(配列やリスト)の並べ替えたものを、さらに並べ替えたい!
    www.urablog.xyz