C#で配列を昇順・降順にソートする方法|Array.SortとCompareの活用術

システム開発
スポンサーリンク
スポンサーリンク

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 は中で比較を繰り返して「どちらが先か」を決めています。

この「比較」が後半の CompareIComparer<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)のソート例

次は UserAge を基準にソートしてみます。

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) < 0x を先(左)に置く
  • Compare(x, y) = 0同順位(順序は固定されないこともある)
  • Compare(x, y) > 0y を先(左)に置く

ここで混乱しやすいのが、「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.SortArray.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>) を使えます

    配列で理解できていれば、コレクションが変わっても迷いません

システム開発
スポンサーリンク
シェアする
tobotoboをフォローする

コメント

タイトルとURLをコピーしました