C#で配列を並び替える処理は、画面表示の整形・ランキング作成・ログの時系列整理など、実務のあらゆる場面で登場します。Array.Sort を起点に、Compare(比較ロジック)を理解しておくと、降順やプロパティ指定のソートも「迷わず書ける」ようになります。
本記事は LINQを使わない“配列”のソートに絞り、基本から IComparer<T> を使った実装まで、コードと動作イメージをセットで解説します。
C#で配列を昇順にソートする基本
この章では、いちばん基本の Array.Sort() による昇順ソートを扱います。まずは「どう書くか」と「何が起きているか」を押さえると、後半の比較関数や IComparer<T> が一気に理解しやすくなります。
✅ Array.Sort() を使った int 型のソート例
C#の例(int配列を昇順ソート)
int[] numbers = {5,1,9,3,7 };
Array.Sort(numbers);
Console.WriteLine(string.Join(", ", numbers));
// 1, 3, 5, 7, 9
ポイント
Array.Sortは配列そのものを並び替えます(破壊的変更)- 返り値はなく、
numbersの中身が並び替わります intのような基本型は “自然な順序(昇順)” が定義されているので、そのままソートできます
✅ シンプルな使い方と内部的な動作の概要
Array.Sort は中で比較を繰り返して「どちらが先か」を決めています。
この「比較」が後半の Compare/IComparer<T> に直結します。
ざっくりイメージ
- 要素Aと要素Bを取り出す
- 「AはBより小さい?同じ?大きい?」を判定する
- その結果をもとに並び順を決める
つまり、ソートの本体は 比較ロジック(大小判定) です。
次章以降は、この比較をどう制御するかを掘り下げます。
降順にソートするには?Reverseと比較関数の使い分け
この章では、降順に並び替える代表的な2パターンを紹介します。単純に「逆順にしたい」だけなら Reverse で十分なことも多い一方、比較関数で降順にするほうが都合が良いケースもあります。
✅ Array.Sort() + Array.Reverse() の基本パターン
C#の例(昇順→反転で降順)
int[] numbers = {5,1,9,3,7 };
Array.Sort(numbers);
Array.Reverse(numbers);
Console.WriteLine(string.Join(", ", numbers));
// 9, 7, 5, 3, 1
使いどころ
- ✅ 実装がシンプルで読みやすい
- ✅ 「昇順の結果を逆にする」だけで目的達成できるとき
- ⚠️ 2回処理(Sort→Reverse)になるので、極端に大きい配列で速度がシビアなら比較関数の降順も検討
✅ 比較関数を使った降順ソートの方法
Array.Sort には比較デリゲート(Comparison<T>)を渡せます。
降順にしたいなら、比較の向きを逆にするのが基本です。
C#の例(比較関数で降順)
int[] numbers = {5,1,9,3,7 };
Array.Sort(numbers, (a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(", ", numbers));
// 9, 7, 5, 3, 1
使いどころ
- ✅ 1回の
Sortで完結(Reverse不要) - ✅ 「降順だけど、条件を追加したい」など拡張しやすい
- ⚠️ ラムダが複雑になると読みづらくなるので、
IComparer<T>が有利
Compareを使ったソートの具体例
ここでは「比較は何を返せばいいのか」を Compare を使って整理します。CompareTo との違いも押さえると、Array.Sort の引数で迷いにくくなります。
✅ Comparer<T>.Default.Compare() の使い方
Comparer<T>.Default.Compare(x, y) は、型Tの既定の比較ルールで大小を判定します。
int なら数値として、string なら文字列の既定の順序として比較されます。
C#の例(Default.Compareで昇順)
int[] numbers = {5,1,9,3,7 };
Array.Sort(numbers, (a, b) => Comparer<int>.Default.Compare(a, b));
Console.WriteLine(string.Join(", ", numbers));
// 1, 3, 5, 7, 9
✅ 昇順・降順の切り替え方法(int 型)
Compare(a, b) と Compare(b, a) を入れ替えるだけで、昇順/降順を切り替えできます。
C#の例(Default.Compareで降順)
int[] numbers = {5,1,9,3,7 };
Array.Sort(numbers, (a, b) => Comparer<int>.Default.Compare(b, a));
Console.WriteLine(string.Join(", ", numbers));
// 9, 7, 5, 3, 1
ポイント
- ✅ 昇順:
Compare(a, b) - ✅ 降順:
Compare(b, a)(引数を逆にするだけ)
✅ カスタムクラス(User)のソート例
次は User の Age を基準にソートしてみます。
Array.Sort(users, (x, y) => ...) の形で プロパティ指定ができます。
C#の例(User.Ageで昇順)
publicclassUser
{
publicstring Name {get;set; } ="";
publicint Age {get;set; }
}
User[] users =
{
new User { Name ="Aki", Age =30 },
new User { Name ="Mika", Age =24 },
new User { Name ="Sora", Age =30 },
};
Array.Sort(users, (x, y) => Comparer<int>.Default.Compare(x.Age, y.Age));
foreach (var uin users)
{
Console.WriteLine($"{u.Name}:{u.Age}");
}
// Mika: 24
// Aki: 30
// Sora: 30
注意点
Ageが同じ場合は「同順位」扱いになるので、並びが固定されるとは限りませんもし Ageが同じならNameで… のようにしたいなら複数条件ソートが必要です(最後の章で触れます)
✅ CompareTo との違いと使い分け
CompareTo
- 例:
a.CompareTo(b) - “aの側”が比較の主導権を持ちます(インスタンスメソッド)
Comparer<T>.Default.Compare
- 例:
Comparer<int>.Default.Compare(a, b) - “外側”から比較を呼び出します(比較器が主導)
使い分けの感覚
- ✅ 手元の型が
IComparable<T>を実装していて、単純比較ならCompareToでもOK - ✅ null安全性や拡張性を意識するなら
Comparer<T>.Default.Compareが扱いやすい - ✅ 条件が増える/再利用したいなら
IComparer<T>が強い
LINQを使わずにソートする実装パターン
この章では、LINQを使わずに配列を並び替える代表パターンを整理します。実務ではパフォーマンスや割り当て(メモリ)を気にして LINQ を避ける場面もあるので、ここで選択肢を明確にしておきます。
✅ Array.Sort + ラムダ式のパターン
C#の例(ラムダでAge昇順)
Array.Sort(users, (x, y) => x.Age.CompareTo(y.Age));
特徴
- ✅ その場で完結、短い
- ⚠️ 条件が増えるとラムダが読みにくくなる(複数条件、null考慮など)
✅ Array.Sort + IComparer<T> 実装クラスによるパターン
C#の例(Comparerクラスを渡す)
Array.Sort(users,new AgeAscComparer());
特徴
- ✅ 比較ロジックをクラスに閉じ込められる(再利用しやすい)
- ✅ テストしやすい(Comparer単体のテストが可能)
- ✅ 複数箇所で同じソート条件を使うなら最適
✅ LINQを避ける理由とメリット
LINQは便利ですが、状況によっては避けたいことがあります。
LINQを避ける典型理由
- ✅ 配列をそのまま並び替えたい(
OrderByは基本的に新しい並びを作る) - ✅ 余計な割り当てを避けたい(大量データ、GC負荷の抑制)
- ✅ 低レイヤー寄りの処理で依存を増やしたくない
- ✅ 比較ロジックを明示的に管理
✅ LINQを使うとこう書ける(LINQで昇順・降順に並び替える例)
C#の例(Age昇順)
var sorted = users.OrderBy(u => u.Age).ToArray();
C#の例(Age降順)
var sorted = users.OrderByDescending(u => u.Age).ToArray();
【サンプルコード】IComparerを使った昇順・降順ソート
ここが一番つまずきやすいポイントなので、かなり丁寧にいきます。IComparer<T> は「ソート中に何度も呼ばれる比較関数を、クラスとして切り出す仕組み」です。
重要なのは “いつ、誰が、どの2つを比べるかはソートアルゴリズム側が決める” という点で、Comparerは 呼ばれたときに正しく答えるだけ です。
✅ AgeAscComparer:User の年齢を昇順で比較
C#の例(昇順Comparer)
publicclassUser
{
publicstring Name {get;set; } ="";
publicint Age {get;set; }
}
publicsealedclassAgeAscComparer :IComparer<User>
{
publicintCompare(User? x, User? y)
{
// null を後ろに寄せたい例(要件で調整してください)
if (ReferenceEquals(x, y))return0;
if (xisnull)return1;
if (yisnull)return-1;
return Comparer<int>.Default.Compare(x.Age, y.Age);
}
}
使い方
User?[] users =
{
new User { Name ="Aki", Age =30 },
null,
new User { Name ="Mika", Age =24 },
new User { Name ="Sora", Age =30 },
};
Array.Sort(users,new AgeAscComparer());
foreach (var uin users)
{
Console.WriteLine(uisnull ?"(null)" :$"{u.Name}:{u.Age}");
}
ここがポイント
- ✅
Compare(x, y)は 「xが先ならマイナス」「同じなら0」「yが先ならプラス」 を返します - ✅ nullをどう扱うかは要件次第なので、Comparerに集約しておくと現場で強いです
✅ AgeDescComparer:User の年齢を降順で比較
降順はとてもシンプルで、比較の順序を逆にするだけです。
C#の例(降順Comparer)
publicsealedclassAgeDescComparer :IComparer<User>
{
publicintCompare(User? x, User? y)
{
if (ReferenceEquals(x, y))return0;
if (xisnull)return1;
if (yisnull)return-1;
// 降順:y.Age と x.Age を比較する
return Comparer<int>.Default.Compare(y.Age, x.Age);
}
}
✅ 昇順・降順をパラメータで切り替えられるComparer
C#の例(1クラスで昇順/降順対応)
publicenum SortDirection
{
Asc,
Desc
}
publicsealedclassUserAgeComparer :IComparer<User>
{
privatereadonly SortDirection _direction;
publicUserAgeComparer(SortDirection direction)
{
_direction = direction;
}
publicintCompare(User? x, User? y)
{
if (ReferenceEquals(x, y))return0;
if (xisnull)return1;
if (yisnull)return-1;
int result = Comparer<int>.Default.Compare(x.Age, y.Age);
return _direction == SortDirection.Asc
? result
: -result;
}
}
使い方
Array.Sort(users,new UserAgeComparer(SortDirection.Asc));
Array.Sort(users,new UserAgeComparer(SortDirection.Desc));
✅ ここが重要:IComparerの「役割」
IComparer<T> は 並び替えをしません。
やっていることは、常に次の質問に答えるだけです。
「x と y、どちらを先に置くべき?」
- マイナス → xを先
- 0 → 同じ
- プラス → yを先
並び替えの実行は Array.Sort 側の責務です。
✅ 比較ロジックの流れと動作解説(ここが肝)
Array.Sort(users, comparer) を呼ぶと、内部で 「いまはこの2要素を比べて」 という呼び出しが何度も発生します。
Comparerがやることは、呼ばれるたびに Compare(x, y) の規約どおりに返すだけです。
Compareの規約(これだけは暗記でOK)
Compare(x, y) < 0→ x を先(左)に置くCompare(x, y) = 0→ 同順位(順序は固定されないこともある)Compare(x, y) > 0→ y を先(左)に置く
ここで混乱しやすいのが、「Compareは並び替えをする関数ではない」 という点です。
Compareは、あくまで “順序の答え” を返します。並び替えの実作業は Array.Sort 側が担当します。
イメージ(会話にすると)
- Sort:「
Aki(30)とMika(24)、どっち先?」 - Comparer:「Mikaが先(=マイナス返す)」
- Sort:「了解、じゃあMikaを左に寄せる」
これを色々な組み合わせで繰り返して、最終的に全体が整列します。
✅ 並び替え結果と動作の具体的なシミュレーション
実際のソート内部はアルゴリズム都合で比較の順番が変わりますが、理解のために 比較が起きる典型の流れ を例で追ってみます。
入力(Age昇順)
- Aki(30)
- Mika(24)
- Sora(30)
Comparerは AgeAscComparer とします。
よくある比較の例(順番は一例です)
- 比較①:Compare(Aki30, Mika24)
Compare(30, 24)→ プラス- 結果:Mikaが左へ寄る
- 比較②:Compare(Sora30, Aki30)
Compare(30, 30)→ 0- 結果:同順位(Aki/Soraの相対順は確定しないことがある)
- 比較③:Compare(Sora30, Mika24)
Compare(30, 24)→ プラス- 結果:Mikaが先
結果
- Mika(24)
- Aki(30)
- Sora(30)
ここで大事なのは、同じAge同士(30と30)を0で返すと、AkiとSoraの順番が “入力順のまま” になる保証はありません。
もし順番を安定させたい/意味ある順にしたいなら、Comparerの中で条件を足します。
例:Ageが同じならNameで昇順
publicsealedclassAgeThenNameAscComparer :IComparer<User>
{
publicintCompare(User? x, User? y)
{
if (ReferenceEquals(x, y))return0;
if (xisnull)return1;
if (yisnull)return-1;
int byAge = Comparer<int>.Default.Compare(x.Age, y.Age);
if (byAge !=0)return byAge;
return StringComparer.Ordinal.Compare(x.Name, y.Name);
}
}
ここまで理解できると、IComparer<T> が「ただの比較係」ではなく、実務での並び替え仕様を一箇所に閉じ込める“設計ポイント”だと見えてきます。
比較ロジックの仕組みとベストプラクティス
この章では、Compare(x, y) の意味をもう一段クリアにして、実装ミスを減らすコツをまとめます。比較の向きを間違えると、ソート結果が逆になったり、極端なケースで例外や不安定動作につながるので要注意です。
✅ Compare(x, y) の戻り値と意味
戻り値の意味(再掲)
- 負:xが先
- 0:同じ(同順位)
- 正:yが先
覚え方
- 「返り値の符号は “xを左に寄せたい度”」くらいの感覚でOKです
✅ 昇順:x → y、降順:y → x の理由
昇順は「小さいものを左」に置きたいので、
Compare(x, y):xが小さいほど負になり、左に行きやすい
降順は「大きいものを左」に置きたいので、
Compare(y, x):比較を逆にして、結果の向きを反転させる
実装の最短ルール
- ✅ 昇順:
Compare(x.Key, y.Key) - ✅ 降順:
Compare(y.Key, x.Key)
✅ Comparer<T>.Default.Compare の利点と null 安全性
Comparer<T>.Default を使う利点は、型Tの既定の比較を統一的に呼べることです。
また、Comparer側でnullの扱いを決めておくと、ソート呼び出し側がすっきりします。
ベストプラクティス(よく使う方針)
- ✅ nullは末尾に寄せる(UI表示や欠損データの扱いで自然なことが多い)
- ✅ 比較キーは
Comparer<キー型>.Default.Compareで統一 - ✅ 文字列は
StringComparer.Ordinalなどを明示(カルチャ依存を避けたい場合)
✅ IComparer<T> の活用場面と利点
IComparer<T> が特に強い場面
- ✅ 同じソート条件を複数箇所で使う(画面、API、バッチなど)
- ✅ 複数条件ソート(Age → Name → Id など)
- ✅ nullや例外ケースの扱いを一元化したい
- ✅ テスト可能な形で比較仕様を固定したい
設計的な嬉しさ
- 呼び出し側は
Array.Sort(users, new XxxComparer())だけで済む - 仕様変更(例:nullは先頭、Ageが同じならName降順など)がComparerの修正に閉じる
まとめ:CompareとIComparerで柔軟なソート処理を実現しよう
最後に、この記事の要点を実務目線で整理します。Array.Sort を「ただの昇順関数」で終わらせず、比較ロジックを味方にすると、配列ソートの表現力が一気に上がります。
✅ 昇順・降順の基本ロジックの整理
- 昇順:
Compare(x, y)(小さいものを左へ) - 降順:
Compare(y, x)(比較の向きを逆にする) Array.Sortは 比較結果に従って並び替える実行役、Comparerは 順序の回答役です
✅ 配列ソートの選択肢と実務での使い分け
- 最短で昇順:
Array.Sort(array) - 手早く降順:
Array.Sort→Array.Reverse - 降順を1回で:
Array.Sort(array, (a,b) => b.CompareTo(a)) - 再利用・複数条件・null対応:
IComparer<T>が本命
✅ 再利用可能な比較クラスの設計アイデア
AgeAscComparer/AgeDescComparerのように 意図が名前で伝わる形にする- 「同点処理(Ageが同じならName)」までComparerに含める
- null方針をComparerに固定して、呼び出し側をシンプルに保つ
✅ 応用編(複数条件ソートやList対応)への展望
- 複数条件:Comparer内で「まずAge、同じならName」のように比較を積み上げる
List<T>でも考え方は同じで、list.Sort(IComparer<T>)を使えます配列で理解できていれば、コレクションが変わっても迷いません


コメント