LoginSignup
6
4

More than 3 years have passed since last update.

【.NET】Listに対する自由自在な動的ソートを実現する

Last updated at Posted at 2020-05-03

こんな動的ソートを実現します

以下のすべての点を満たすソート処理を作ります。

  • 文字列で指定されたプロパティの値でListをソートする。
  • Listを構成する各インスタンスに含まれる深い階層のインスタンスのプロパティも、ソートキーとして指定できる。
  • Linqの OrderBy 句等の使用に慣れている人にも馴染みやすいよう、Linqそっくりな使い方のメソッドも用意する。

例えば、以下のようなListをソート対象と仮定します。

var targetList = new List<SomeClass>
{
    new SomeClass // This is first item.
    {
        Prop1 = 123,
        Prop2 = 45.6,
        Prop3 = "ABC",
        Prop4 = new InnerClass
        {
            InnerProp1 = 23,
            InnerProp2 = "45"
            InnerProp3 = new DeepClass
            {
                DeepProp1 = 6.7
            }
        }
    },
    // ... some other items here ...
    new SomeClass // This is last item.
    {
        Prop1 = 987,
        Prop2 = 65.4,
        Prop3 = "ZYX",
        Prop4 = new InnerClass
        {
            InnerProp1 = 54,
            InnerProp2 = "32"
            InnerProp3 = new DeepClass
            {
                DeepProp1 = 1.0
            }
        }
    },
};

実行したいソートに応じて、以下のようにソート処理を呼び出せるものとします。

  • 使用例1 : このリストを Prop1 で昇順にソートする場合
// ≪メソッド形式≫ ※第3引数はソートの昇順・降順を指定するEnum
Sort(ref targetList, "Prop1", SortType.Asc);

// ≪Linq風形式≫
var sortedList = targetList
    .OrderBy("Prop1", SortType.Asc)
    .ToList();
  • 使用例2 : 深い階層 Prop4.InnerProp3.DeepProp1 で降順にソートする場合
// ≪メソッド形式≫
Sort(ref targetList, "Prop4.InnerProp3.DeepProp1", SortType.Desc);

// ≪Linq風形式≫
var sortedList = targetList
    .OrderBy("Prop4.InnerProp3.DeepProp1", SortType.Desc)
    .ToList();
  • 使用例3
    さらに複雑なソートも動的に実現できるものとします。 例えば、SQLのORDER BY句風に書いた場合に ORDER BY Prop1 ASC, Prop4.InnerProp3.DeepProp1 DESC, Prop4.InnerProp2 DESC となるようなソートをする場合、以下のようにメソッドを呼び出します。
// ≪メソッド形式≫
var sortKeySortTypePairs = new Dictionary<string, SortType>
{
    { "Prop1", SortType.Asc },
    { "Prop4.InnerProp3.DeepProp1", SortType.Desc },
    { "Prop4.InnerProp2", SortType.Desc },
};
Sort(ref targetList, sortKeySortTypePairs);

// ≪Linq風形式≫
var sortedList = targetList
    .OrderBy(new Dictionary<string, SortType>
    {
        { "Prop1", SortType.Asc },
        { "Prop4.InnerProp3.DeepProp1", SortType.Desc },
        { "Prop4.InnerProp2", SortType.Desc },
    })
    .ToList();

汎用性を持たせたいので、(理論上)どれだけ深い階層のプロパティでもソートキーとして指定できるメソッドとして作成します。

このソート処理の使い所は?

.NET Core等で「リクエストに応じて何らかのリストを生成して返すWebAPI」を作る場合、リクエストで指定されたキーでソートされたリストを返したい、という場面が生じることがあります。
.NETでソートを行う際の定番はLinqのOrderBy句やThenBy句等々ですが、これらを素直に使う場合、ソート対象のプロパティは静的に指定することになります。
なので、想定されるソートキーの数だけif、else ifを連結してOrderBy等を実装する、などというゴリ押しな手段を採ることになりかねません。

そこで、このソートキーを動的に指定できるソート処理の出番となります。

例えば、リクエストボディの中に

"sortKey": "Prop4.InnerProp1",
"sortType": "Asc",

という要素が積まれるとして、これらをソート処理の呼び出しに使うようにする、という寸法です。

この動的ソートを実現するNuGetパッケージを作ってみた

上記のソート処理用メソッドを持つNuGetパッケージを作り、公開してみました。
https://www.nuget.org/packages/MagicSort/

対応フレームワーク・対応バージョンは以下の通りです。

  • .NET Core 2.0~
  • .NET Framework 4.5~
  • .NET Standard 2.0~

ソースはGitHubにて公開しています。
https://github.com/microwavePC/MagicSort

詳細な使い方は、GitHubのREADMEを参照してください!

中でこんな処理を行っています

この処理の肝となる部分を簡潔に紹介します。処理の全体はGitHubにupしたソースをご覧ください。

このソート処理の中では、LinqのOrderBy OrderByDescending を使用しています。
複数のキーが指定される場合は、加えてThenBy ThenByDescendingも使用しています。
これらの引数となるラムダ式を、処理内部で動的に生成しています。

単一キーによるソート処理では、以下のように動的ソートを実現しています。

// 変数「targetList」「sortKey」と「sortType」は、メソッドの引数で渡されたものを使用する。
// ジェネリックのTには、このメソッドが呼び出されるときに「targetList」の要素の型が入る。

// ここで呼び出している内部処理「AssembleOrderFunc」で、OrderBy句の引数となるラムダ式を動的に生成する。
Func<T, object> orderFunc = AssembleOrderFunc<T>(sortKey);

switch (sortType)
{
    case SortType.Asc:
        targetList = targetList
            .OrderBy(orderFunc)
            .ToList();
        break;
    case SortType.Desc:
        targetList = targetList
            .OrderByDescending(orderFunc)
            .ToList();
        break;
}

ラムダ式を生成するメソッド AssembleOrderFunc では、以下の処理を行っています。

/// <summary>
/// Assembles the function object for sorting Linq method.
/// </summary>
/// <typeparam name="T">Type of target list class.</typeparam>
/// <param name="sortKey">Sort key.</param>
/// <returns>Function object.</returns>
private static Func<T, object> AssembleOrderFunc<T>(string sortKey)
    where T : class
{
    // ソートキーを「.」で分割する。
    List<string> sortKeyHierarchy = sortKey.Split('.').ToList();

    // リフレクションを使用し、ソート対象となるプロパティ値を引っ張り出す。
    Func<T, object> orderFunc = x =>
    {
        object val = x;
        foreach (string key in sortKeyHierarchy)
        {
            if (val == null)
            {
                return val;
            }

            val = val.GetType().GetRuntimeProperty(key).GetValue(val);
        }

        return val;
    };

    return orderFunc;
}

このソート処理のパフォーマンスは?

実行する環境にも拠るとは思いますが、この1024件のデータを持つ多階層リストを対象に以下のような5重のソート処理を実行したところ、デバッグ実行における処理時間は5~10ミリ秒前後でした。

// メソッド形式
var sortKeySortTypePairs = new Dictionary<string, SortType>
{
    { "Property4.PropertyW.PropertyD.PropertyK.Property4.PropertyW.PropertyD.PropertyI", SortType.Desc },
    { "Property1", SortType.Asc },
    { "Property4.PropertyW.PropertyA", SortType.Desc },
    { "Property2", SortType.Asc },
    { "Property4.PropertyW.PropertyD.PropertyJ", SortType.Asc },
}

MagicSorter.Sort(ref targetList, sortKeySortTypePairs);
// Linq風形式
var sortedList = targetList
    .OrderBy(new Dictionary<string, SortType>
    {
        { "Property4.PropertyW.PropertyD.PropertyK.Property4.PropertyW.PropertyD.PropertyI", SortType.Desc },
        { "Property1", SortType.Asc },
        { "Property4.PropertyW.PropertyA", SortType.Desc },
        { "Property2", SortType.Asc },
        { "Property4.PropertyW.PropertyD.PropertyJ", SortType.Asc },
    })
    .ToList();
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4