みなさんこんにちは、ヒロポンです!
「部署コード順に並べて、その中では入社日が新しい順で」。帳票の並び順って、こういう複数キーの指定、業務系だとしょっちゅう来ますよね??
で、OrderBy 一発で片付けようとして「あれ、2つ目のキーってどう足すんだ」で手が止まる。「特定の部署だけ先頭に固定したいんだけど」でまた止まる。ん?そもそも C# のソートって IComparer とか Comparison とか OrderBy とか、選択肢が多すぎて使い分けが分からんくない?
今回は C# のソートを IComparer / Comparison / OrderBy の3パターンで書く方法と、その使い方・使い分けを、コピペで動くコードで押さえます。単純な1キーなら OrderBy、独自ルールを何度も使うなら IComparer。判断軸まで一気にいきます。
結論: 1キーなら OrderBy、複数キーや独自ルールなら Comparison か IComparer
先に結論だけ。
- 単純な並び替え(1キー) なら
OrderBy/OrderByDescendingでいい。読みやすいし、後で書くけど安定ソートなので元の順序も壊さない。 - 複数キーなら
OrderBy(...).ThenBy(...)を重ねるか、List.SortにComparisonラムダを渡す。 - 「特定コードだけ先頭固定」みたいな独自ルールを、しかも何度も使い回すなら
IComparer<T>をクラスで実装するのが一番きれい。
IComparer<T> は、2つの要素の大小関係を Compare メソッド1つで定義する「並び替えルールの部品」。一度作れば List.Sort にも Array.Sort にも OrderBy にも渡せます。ここが IComparer の使い方の勘所。
判断軸を先に表で見ておきます。

この表の「安定ソート」と「元コレクション」の行、ここが業務系で地味に事故る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 以外なら(=差がついたら)その時点で return。0(=同値)だったら2つ目のキー(入社日)に進む。この「差がついたら即 return、同値なら次のキー」がデリゲート方式の型です。
もう1つ、降順にしたいキーは b.CompareTo(a) と逆にする。a.HireDate.CompareTo(b.HireDate) が昇順、b と a をひっくり返すと降順。ここ、慣れないと符号を間違えて「なんで逆順なんや??」ってなるやつ。
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>.Sort と Array.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.Sort は master も並べ替えます。「帳票の並びをいじったら、なぜか一覧画面の順番も変わった」。これ、原因これです。
元を残したいなら、OrderBy で新しいリストを作るか、master.ToList() で複製してから Sort する。
var forReport = master.OrderBy(e => e.DeptCode).ToList(); // master は無傷
③「a – b」の減算で比較しない(符号ミスとオーバーフロー)
比較ラムダを a - b みたいな引き算で書くのは、やめといたほうがいい。理由は2つ。
1つは、値が int.MinValue 近辺だと減算がオーバーフローして符号が反転し、大小が逆に出ることがある。もう1つは、単純に符号を間違えて昇順・降順が逆になる事故が起きやすい。
CompareTo を使えばどっちも起きません。下の diff が「素朴だけど危ない書き方」→「安定した複数キー」への書き換えです。

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

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 なら OrderBy と ThenBy を重ねるだけです。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 を実装)で、Sort や OrderBy に渡します。1つの型で並び順を何通りも切り替えたいなら、IComparer を複数用意するのが定石です。
特定のコードだけ先頭に固定したい場合はどう書きますか?
「そのコードなら 0、それ以外は 1」というグループ用のキーを先に比較すれば先頭固定になります。IComparer の Compare 内で最初にそのグループキーを比べるか、OrderBy(e => e.DeptCode == 99 ? 0 : 1) のように OrderBy の第1キーへ三項演算子で書くのが手軽です。
次に読むべき記事





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



