CodeGym /コース /C# SELF /コレクションのソート

コレクションのソート

C# SELF
レベル 29 , レッスン 2
使用可能

1. はじめに

もし靴下がいっぱい入った引き出しからペアを探したことがあるなら、もうソートの問題にぶち当たったことがあるはず。全部ぐちゃぐちゃだと、欲しいものを探すのはまるでクエストだよね。プログラミングでコレクションを扱うときも同じ感じ。

ソートってのは、コレクションの要素を何かの基準(例えばアルファベット順、値、日付とか)で並べ替えること。これが大事なのは:

  • ユーザーにデータを見せるとき(カオスは誰も好きじゃない)。
  • 検索を簡単にするため(例えばバイナリサーチはソート済みコレクションだけで使える)。
  • 比較、レポート、エクスポートとか他の操作のため。

100万ドルの質問:「要素が5個しかなかったら、ソートしなくてもいい?」— 理論的にはOK。でも5が500や5000になったら、自動化なしじゃ無理ゲーだよ。

.NETにはソートのやり方が2つある:

  • 元のコレクションを変える(例えばList<T>Sortメソッドでソート)。
  • 新しいソート済みコピーを作る(例えば配列をクローンしてからソート)。

.Sort()メソッドでリストをソートする

Sort()メソッドはList<T>クラスにあるよ。このクラスはIList<T>インターフェースを実装してて、インデックスアクセスや要素の順番変更ができる。

例 — 数字を昇順でソート:


using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var numbers = new List<int> { 5, 2, 9, 1, 5, 6 };
        numbers.Sort();
        Console.WriteLine("昇順でソート:");
        foreach(var number in numbers)
        {
            Console.Write($"{number} "); // 1 2 5 5 6 9
        }
    }
}

ここでは「インプレース」ソートだよ:元のリストnumbersが変わって、要素の順番が入れ替わる。

文字列のソート

文字列もバッチリソートできるよ:


var words = new List<string> { "オレンジ", "りんご", "バナナ", "なし" };
words.Sort();
Console.WriteLine(string.Join(", ", words)); // オレンジ, バナナ, なし, りんご

豆知識:デフォルトだと文字列のソートはUnicode順で行われる。「人間の」アルファベット順じゃないから、多言語アプリでは注意してね。

2. 独自ルールでソートする

デフォルトのソートじゃダメなときもある。例えばユーザーを名前じゃなくて年齢や登録日でソートしたい場合とか。

ラムダ式でソート(Sort(Comparison<T>)メソッド)

Sortメソッドは、2つの要素を比べる関数(比較ルール)を渡して呼び出せるよ。

ユーザーを年齢でソート:


class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// ...

var users = new List<User>
{
    new User { Name = "アリサ", Age = 30 },
    new User { Name = "ボブ", Age = 25 },
    new User { Name = "エバ", Age = 35 }
};

users.Sort((u1, u2) => u1.Age.CompareTo(u2.Age));

foreach (var user in users)
{
    Console.WriteLine($"{user.Name}: {user.Age}");
}
// 出力:
// ボブ: 25
// アリサ: 30
// エバ: 35

このマジックはどう動く?ラムダ(u1, u2) => u1.Age.CompareTo(u2.Age)は、u1が若ければマイナス、年上ならプラス、同じなら0を返す。

IComparer<T>インターフェースを使う

比較ルールを別クラスにしたいときもある。コレクションが多かったり、ロジックが複雑なときに便利。


class UserAgeComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        return x.Age.CompareTo(y.Age);
    }
}

// ...

var users = new List<User>{ /* ... */ };
users.Sort(new UserAgeComparer()); // これで年齢順ソート

いろんな場所で同じソートが必要なときや、ソートルールがたくさんあるときに便利だよ。

複数フィールドで手動ソート

ユーザーの年齢が同じ場合、同じ年齢グループ内で名前順にしたいとき:


users.Sort((u1, u2) => {
    int ageCompare = u1.Age.CompareTo(u2.Age);
    if (ageCompare != 0)
        return ageCompare;
    else
        return u1.Name.CompareTo(u2.Name);
});

これでまず年齢、次に名前でソートされるよ。

3. コレクションのコピーをソート(元を変えない)

元のリストを変えたくないなら、ソート前にコピーしよう:


var copy = new List<int>(numbers);
copy.Sort();

カスタムオブジェクトでも同じ:


var usersCopy = new List<User>(users);
usersCopy.Sort((a, b) => a.Age.CompareTo(b.Age));

4. 配列のソート

配列(T[])も超シンプル:


int[] numbers = { 4, 2, 9, 7 };
Array.Sort(numbers); // 元の配列が変わる

「カスタム」ソートの場合:


Array.Sort(numbers, (a, b) => b.CompareTo(a)); // 降順ソート

Array.Sortは元の配列を変えるだけで、新しい配列は返さないから注意。元を残したいなら、先にコピーしよう:


int[] oldNumbers = { 3, 2, 1 };
int[] copy = (int[])oldNumbers.Clone();
Array.Sort(copy);

5. ディクショナリ(Dictionary<TKey, TValue>)の「ソート」

Dictionary<TKey, TValue>は本質的に順序なしコレクション(つまり列挙時の順番は保証されない)。でも「ソート済みペア」が欲しいときもあるよね。

ソート済みのキーや値が欲しいなら、リストを作ってペアをコピーして、好きな方法でソートしよう:


var dict = new Dictionary<string, int>
{
    { "りんご", 2 },
    { "オレンジ", 5 },
    { "なし", 3 }
};

// キーでソート:
var keyValueList = new List<KeyValuePair<string, int>>(dict);
keyValueList.Sort((a, b) => a.Key.CompareTo(b.Key));
foreach (var kv in keyValueList)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

// 値でソート:
keyValueList.Sort((a, b) => a.Value.CompareTo(b.Value));
foreach (var kv in keyValueList)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

ソート済みのキーや値だけ欲しい場合:


var sortedKeys = new List<string>(dict.Keys);
sortedKeys.Sort();

var sortedValues = new List<int>(dict.Values);
sortedValues.Sort();

でもDictionary自体の構造は変わらないよ — ただソート済みの列挙を得るだけ。.NET 9ではOrderedDictionary<TKey, TValue>が登場して、要素の順番を保持できるようになる。あまり使わないけど、たまに必要なときがある。

6. 便利なポイント

比較インターフェースは必須?

超シンプル:自分のオブジェクトが「どう比べるか」を知っててほしいなら(例えば日付や名前で)、IComparable<T>インターフェースを実装しよう。


class Product : IComparable<Product>
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public int CompareTo(Product other)
    {
        return Price.CompareTo(other.Price);
    }
}

これでこうできる:


var products = new List<Product> { /* ... */ };
products.Sort(); // 値段順でソート

デフォルトの比較が気に入らないなら、IComparer<T>やラムダを使えばOK。

ユーザーリストをアルファベット順でソート

アプリにユーザーリストがあるとする:


List<string> users = new List<string> { "ビクトル", "アンナ", "エカテリナ", "ボリス" };
users.Sort(); // これでusersは:アンナ, ボリス, ビクトル, エカテリナ

タスクを優先度で並べる


class Task
{
    public string Title { get; set; }
    public int Priority { get; set; } // 1 - 緊急, 2 - 重要, 3 - 後回しOK
}

var todo = new List<Task>
{
    new Task { Title = "宿題をやる", Priority = 2 },
    new Task { Title = "パンを買う", Priority = 1 },
    new Task { Title = "ドラマを見る", Priority = 3 }
};

todo.Sort((a, b) => a.Priority.CompareTo(b.Priority));

foreach (var task in todo)
    Console.WriteLine($"{task.Priority}: {task.Title}");

ソート方法の比較

コレクション インプレースで変わる? ソートメソッド ルール指定できる?
List<T>
はい
Sort()
はい:ラムダやIComparer
T[]
はい
Array.Sort()
はい:ラムダやIComparer
Dictionary<TKey, TValue>
いいえ —(リスト作ってソート) はい:ラムダやIComparerで

7. よくあるミスやソートの注意点

インプレースソートは元のコレクションを変えるよ!元データを変えたくないなら、先にコピーしよう。

変更不可コレクション(例:ReadOnlyCollection<T>)をソートしようとすると実行時エラーになる。

文字列の比較はカルチャによって違う:文字列ソート(特にキリル文字やウムラウトなど)はロケールによって結果が変わることがある。正しくソートしたいならComparer.Create(...)でカルチャを指定しよう。

ディクショナリのソートは構造を変えない — いつも新しいペアの列挙(または新しいリスト)を返すだけ。

複雑な順序が必要ならIComparer<T>インターフェースで自分のロジックを使おう。

間違ったソート例(実践的な落とし穴):


var numbers = new List<int> { 1, 2, 3 };
var sorted = numbers.Sort(); // エラー:Sort()はvoidを返す!

正しいやり方:


numbers.Sort(); // numbers自体が変わる

// 新しいコレクションが欲しい場合:
var sorted = new List<int>(numbers);
sorted.Sort();
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION