動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

【C#】複数キー・独自ルールのソートを実装する3パターン(IComparer / Comparison / OrderBy)

バイブス父さん
現役の業務SE
2026年9月7日16 min read広告 (PR) を含む場合があります
【C#】複数キー・独自ルールのソートを実装する3パターン(IComparer / Comparison / OrderBy)

みなさんこんにちは、ヒロポンです!

「部署コード順に並べて、その中では入社日が新しい順で」。帳票の並び順って、こういう複数キーの指定、業務系だとしょっちゅう来ますよね??

で、OrderBy 一発で片付けようとして「あれ、2つ目のキーってどう足すんだ」で手が止まる。「特定の部署だけ先頭に固定したいんだけど」でまた止まる。ん?そもそも C# のソートって IComparer とか Comparison とか OrderBy とか、選択肢が多すぎて使い分けが分からんくない?

今回は C# のソートを IComparer / Comparison / OrderBy の3パターンで書く方法と、その使い方・使い分けを、コピペで動くコードで押さえます。単純な1キーなら OrderBy、独自ルールを何度も使うなら IComparer。判断軸まで一気にいきます。

結論: 1キーなら OrderBy、複数キーや独自ルールなら Comparison か IComparer

先に結論だけ。

  • 単純な並び替え(1キー) なら OrderBy / OrderByDescending でいい。読みやすいし、後で書くけど安定ソートなので元の順序も壊さない。
  • 複数キーなら OrderBy(...).ThenBy(...) を重ねるか、List.SortComparison ラムダを渡す。
  • 「特定コードだけ先頭固定」みたいな独自ルールを、しかも何度も使い回すなら IComparer<T> をクラスで実装するのが一番きれい。

IComparer<T> は、2つの要素の大小関係を Compare メソッド1つで定義する「並び替えルールの部品」。一度作れば List.Sort にも Array.Sort にも OrderBy にも渡せます。ここが IComparer の使い方の勘所。

判断軸を先に表で見ておきます。

C#の3つのソート方式(List.Sort+Comparison / IComparer実装 / LINQ OrderBy)を手軽さ・複数キー・再利用・安定性・破壊性で比較した表

この表の「安定ソート」と「元コレクション」の行、ここが業務系で地味に事故る2点です。後半のハマりポイントで詳しくいきます。

まずは3パターンを順に書いていきましょう。共通の題材として、こんな感じの社員データを使います。

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

class Employee
{
    public int DeptCode { get; set; }      // 部署コード
    public string EmpCode { get; set; }    // 社員コード
    public DateTime HireDate { get; set; } // 入社日

    public Employee(int deptCode, string empCode, DateTime hireDate)
    {
        DeptCode = deptCode;
        EmpCode = empCode;
        HireDate = hireDate;
    }

    public override string ToString()
    {
        return $"Dept={DeptCode} Emp={EmpCode} Hire={HireDate:yyyy-MM-dd}";
    }
}

パターン1: List.Sort に Comparison を渡す(その場で複数キー)

一番手軽なのがこれ。List<T>.Sort にラムダ(Comparison<T> デリゲート)を1個渡すだけ。

// 部署コード昇順 → 同じ部署の中では入社日が新しい順(降順)
employees.Sort((a, b) =>
{
    int byDept = a.DeptCode.CompareTo(b.DeptCode);
    if (byDept != 0) return byDept;            // 部署で差がつけばそこで確定
    return b.HireDate.CompareTo(a.HireDate);   // 部署が同じなら入社日で。降順なので b と a を逆に
});

ポイントは2つ。

1つ目のキー(部署)を比較して、0 以外なら(=差がついたら)その時点で return0(=同値)だったら2つ目のキー(入社日)に進む。この「差がついたら即 return、同値なら次のキー」がデリゲート方式の型です。

もう1つ、降順にしたいキーは b.CompareTo(a) と逆にするa.HireDate.CompareTo(b.HireDate) が昇順、ba をひっくり返すと降順。ここ、慣れないと符号を間違えて「なんで逆順なんや??」ってなるやつ。

List.Sort は手軽なんですが、元の employees を直接並べ替えます(破壊的)。ここが後で効いてくるので頭の隅に置いといてください。

パターン2: IComparer<T> を実装(独自ルール・使い回し)

「管理部門(部署コード99)は帳票の一番上に固定、あとは部署コード順、同じ部署なら社員コード順」。こういう独自ルールで、しかも複数の画面や帳票で同じ並びを使い回したい。そういう時は IComparer<T> をクラスで実装します。

// 「99(管理部門)を先頭に固定 → 部署コード昇順 → 同部署は社員コード順」
public class DeptPriorityComparer : IComparer<Employee>
{
    public int Compare(Employee x, Employee y)
    {
        // 99 を最優先にするため、99 なら 0・それ以外は 1 のグループキーを先に比較
        int gx = x.DeptCode == 99 ? 0 : 1;
        int gy = y.DeptCode == 99 ? 0 : 1;
        int byGroup = gx.CompareTo(gy);
        if (byGroup != 0) return byGroup;

        int byDept = x.DeptCode.CompareTo(y.DeptCode);
        if (byDept != 0) return byDept;

        // 社員コードは序数(Ordinal)で。カルチャ依存にしない意図を明示
        return string.Compare(x.EmpCode, y.EmpCode, StringComparison.Ordinal);
    }
}

使う側はこう。

// List.Sort に渡す
employees.Sort(new DeptPriorityComparer());

// 同じ Comparer を OrderBy にもそのまま渡せる(要素そのものをキーにする)
var sorted = employees
    .OrderBy(e => e, new DeptPriorityComparer())
    .ToList();

いい感じですよね。ルールを1回クラスにしておけば、List.Sort でも OrderBy でも Array.Sort でも同じ並びを使い回せる。「並び順のロジックが画面ごとにコピペで散らばる」問題が消えます。これが IComparer を選ぶ最大の理由。

ちなみに IComparer<T>(外から渡す比較部品)とよく混同されるのが IComparable<T>(型自身が持つ自然な並び順)。employees.Sort()引数なしで呼ぶと、Employee が IComparable<Employee> を実装してればその CompareTo が使われます。「この型のデフォルトの並びはこれ」を1個決めたいなら IComparable、「並びを何通りも切り替えたい」なら IComparer を複数。こう覚えておくと迷いません。

パターン3: LINQ の OrderBy / ThenBy(宣言的・安定・非破壊)

複数キーを一番読みやすく書けるのが LINQ。OrderBy で1つ目、ThenBy / ThenByDescending で2つ目を足すだけ。

// 部署コード昇順 → 同部署は入社日降順、を宣言的に
var sorted = employees
    .OrderBy(e => e.DeptCode)
    .ThenByDescending(e => e.HireDate)
    .ToList();

// 元の employees はそのまま。sorted が並び替え済みの「新しいリスト」

OrderBy元のコレクションを一切いじらず、並び替えた新しいシーケンスを返します(非破壊)。しかも後で書くとおり安定ソート。パターン1の List.Sort とはここが真逆です。

独自ルール(99を先頭固定)も、OrderBy なら第1キーに三項演算子を仕込めば書けます。

var sorted = employees
    .OrderBy(e => e.DeptCode == 99 ? 0 : 1)          // 99 を先頭グループへ
    .ThenBy(e => e.DeptCode)
    .ThenBy(e => e.EmpCode, StringComparer.Ordinal)  // 文字列キーは Comparer を渡せる
    .ToList();

OrderBy / ThenBy はキーごとに StringComparer を渡せるので、パターン2の IComparer とほぼ同じ表現力があります。使い回さない「その帳票だけの並び」なら、クラスを作らずこっちで十分。

💡 LINQ の GroupBy と組み合わせた集計・結合は別記事 C# の LINQ で GroupBy / OrderBy / Join を3パターン で書いたので、今回は並び替え単体に絞っています。

ハマりポイント: 知らないと帳票が「たまに」バグる3つ

ここからが本題かもしれない。業務系のソートで踏むやつを3つ。

① List.Sort / Array.Sort は不安定ソート(同値の順序が保証されない)

これが一番地味で一番怖い。

List<T>.SortArray.Sort不安定ソートです。つまり、ソートキーが同じ値だった要素どうしの、元の並び順は保証されない。要素数やデータ次第で、実行するたびに同値グループの中の順番が変わり得ます。

対して LINQ の OrderBy安定ソート。同値の要素は、元のコレクションでの登場順を保ったまま並びます。これは公式に保証されている挙動。

何が怖いって、「たまに順番が入れ替わる」帳票バグになるから。実際に手元で試すと、十数件くらいまでは崩れず、20件30件と増えたあたりから同値の順序が入れ替わり出しました。これは内部実装(要素が少ないと別アルゴリズムに切り替わる)の都合で「たまたま安定に見えてる」だけ。安定が保証されてるわけじゃない。

だからテスト環境の少ないデータでは気づけず、本番の数万件で崩れる。しかも毎回同じ崩れ方じゃないから再現もしづらい。これが一番タチが悪い。

判断はシンプルです。同値要素の並びに意味があるなら OrderBy を使う。「入力順(=伝票の入力順)を保ったまま部署でまとめたい」みたいな時は、List.Sort を選んだ時点で負けです。

② List.Sort は破壊的、OrderBy は非破壊(参照の共有事故)

パターン1でも触れたやつ。List.Sort元のリスト自体を並べ替えます

これが刺さるのは、同じ List の参照をあちこちで持ち回してる時。

var master = LoadEmployees();   // 画面Aと画面Bで共有しているマスタ
var forReport = master;         // 参照コピー(中身は同じリスト)

forReport.Sort(new DeptPriorityComparer());
// ↑ master も一緒に並び変わってしまう。画面Aの表示順まで巻き込まれる

forReport = master参照のコピーなので、forReport.Sortmaster も並べ替えます。「帳票の並びをいじったら、なぜか一覧画面の順番も変わった」。これ、原因これです。

元を残したいなら、OrderBy で新しいリストを作るか、master.ToList() で複製してから Sort する。

var forReport = master.OrderBy(e => e.DeptCode).ToList(); // master は無傷

③「a – b」の減算で比較しない(符号ミスとオーバーフロー)

比較ラムダを a - b みたいな引き算で書くのは、やめといたほうがいい。理由は2つ。

1つは、値が int.MinValue 近辺だと減算がオーバーフローして符号が反転し、大小が逆に出ることがある。もう1つは、単純に符号を間違えて昇順・降順が逆になる事故が起きやすい。

CompareTo を使えばどっちも起きません。下の diff が「素朴だけど危ない書き方」→「安定した複数キー」への書き換えです。

部署コードを減算で比較する不安定な List.Sort から、OrderBy と ThenByDescending による安定した複数キーソートへの書き換え

ここまでの「不安定ソート」と「減算オーバーフロー」、口で言われても半信半疑だと思うので、実機で回した結果を貼っときます。同じ部署コードの要素を8件と30件でソートした時の並びと、int.MinValue を減算比較に食わせた時の符号反転が、こんな感じで出ます。

List.Sort と OrderBy の安定性の実測と、減算比較のオーバーフロー再現の実行結果

8件だと List.Sort でも崩れてない(=安定に見える)。でも30件だと同値の順番がぐちゃっと入れ替わってるのが分かります。OrderBy は両方きれいに元順序キープ。これが「たまに」バグの正体です。

現場メモ: 並び順は「定石」で押さえると事故が減る

俺も昔、流通系の基幹で帳票の明細が「たまに順番が入れ替わる」というバグを踏みました。仕様どおり部署でソートしてるはずなのに、同じ部署の中の行順が、印刷しなおすと微妙に変わる。

原因はまさに①の不安定ソート。List.Sort を単一キーで呼んでたやつでした。同値の行の順序が実行ごとにブレてた。

テスト環境の十数件では気づけず、本番の数万件で表面化して、半日ログを追ってようやく「あ、Sort が安定じゃないからか」と青ざめた記憶があります。

そこから俺の中では、業務帳票は原則 OrderBy(安定・非破壊)、パフォーマンスや独自ルールで必要な時だけ Comparison / IComparer、というふうに定石化しました。迷ったら OrderBy。これだけで「たまにバグる」系の並び順トラブルはほぼ消えます。

もう1つ現場的なコツ。降順キーが混ざる複数キーソートは、コメントで「何を昇順・何を降順」を1行そえておく。半年後の自分が ThenByDescending を見て「これ何で降順だっけ??」ってなるので。地味だけど効きます。

まとめ

C# のソート3パターン、最後に判断軸だけおさらいします。

  • OrderBy / ThenBy … 読みやすい・安定・非破壊。業務帳票の第一候補。複数キーは ThenBy を足すだけ。
  • List.Sort + Comparison … その場で1回だけ、軽く並べたい時。ただし破壊的で不安定なのを承知で。
  • IComparer<T> …「特定コード先頭固定」みたいな独自ルールを、複数の画面・帳票で使い回す時。ルールをクラスに閉じ込められる。

そして忘れちゃいけない2点、List.Sort は不安定&破壊的、OrderBy は安定&非破壊。これを押さえておくだけで、帳票の並び順まわりの地雷はだいたい避けられます。いい感じに定石で押さえていきましょう!!

よくある質問

C# で複数キーのソートはどう書くのが一番きれいですか?

LINQ なら OrderByThenBy を重ねるだけです。OrderBy(e => e.DeptCode).ThenByDescending(e => e.HireDate) のように、キーを上から順に足していけば複数キーになります。List.Sort で書くなら、Comparison ラムダの中で1つ目のキーを比較し、戻り値が 0(同値)なら2つ目のキーを比較する形です。

List.Sort と OrderBy はどちらを使うべきですか?

元のリストを並べ替えて構わない一度きりの処理なら List.Sort、元を壊したくない・同値要素の順序を保ちたい(安定ソート)なら OrderBy です。List.Sort は破壊的かつ不安定、OrderBy は非破壊かつ安定なので、業務帳票のように「入力順を保ったまま並べたい」ケースは OrderBy が安全です。

IComparer と IComparable の違いは何ですか?

IComparable<T> は型自身に「自然な並び順」を1つ持たせる仕組み(CompareTo を実装)で、employees.Sort() を引数なしで呼んだ時に使われます。IComparer<T> は型の外側に並び替えルールを部品として作る仕組み(Compare を実装)で、SortOrderBy に渡します。1つの型で並び順を何通りも切り替えたいなら、IComparer を複数用意するのが定石です。

特定のコードだけ先頭に固定したい場合はどう書きますか?

「そのコードなら 0、それ以外は 1」というグループ用のキーを先に比較すれば先頭固定になります。IComparer の Compare 内で最初にそのグループキーを比べるか、OrderBy(e => e.DeptCode == 99 ? 0 : 1) のように OrderBy の第1キーへ三項演算子で書くのが手軽です。

次に読むべき記事

以上!

同じ「並び順が謎に崩れる」で溶かしてる人いたら、どんどんシェア待ってるぜ!!


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について