1. はじめに
想像してみて、君は大学の学部長で、めっちゃたくさんの学生がいる。いろんな基準で並べ替えたリストがよく必要になるよね:
- 名前順(アルファベット順で学生をすぐ探したいとき)。
- 平均点順(優秀な学生に奨学金をあげるため)。
- 年齢順(統計やコンテストとかいろいろ)。
- 学年順、その中でさらに名字順。
もしIComparable<T>だけに頼ってたら、学生クラスは一つだけの比較方法しか実装できない。例えば、「自然な」順序を平均点にしたら、List.Sort()はOK。でも、名前順で並べたいときは?学生クラスはもう「点数順」で埋まってるから、2つの「自然な」順序は同時に持てない。ボクシングの「最強になる方法」しか知らないのに、急にチェスで最強になれって言われて、ボクシングのやり方でやろうとする感じ。うまくいかないよね?
こういう、同じ型のオブジェクトをいろんな方法でソートしたい時や、クラス自体に比較ロジックを入れたくない時に使うのがIComparer<T>インターフェース。クラスのソースにアクセスできない場合や、クラスが全部のソート方法を知るべきじゃない時にも便利だよ。
2. インターフェース IComparer<T>
IComparable<T>が「自分自身の感覚」で「自分」と他を比べるのに対して、IComparer<T>は完全に外部の審判とか独立したジャッジみたいなもので、どんな2人(オブジェクト)でも自分のルールで比べるんだ。
例えば、君がサッカーチームのコーチだとする。キャプテンを選ばなきゃいけない。
- IComparable<T>:各選手が「オレはあいつより速く走れるから強い!」って自分ルールで言う。
- IComparer<T>:君が「今日はパスの正確さでキャプテン決めるぞ。ペチャ、パスして!バシャ、パスして!お、ペチャの方が正確だな。今日はペチャがキャプテン!」みたいに、外部のルールで比べる。
定義:IComparer<T>は.NETのインターフェースで、T型の2つのオブジェクトに対して外部の比較ロジックを定義できる。つまり、IComparer<T>を実装したクラスは比較されるオブジェクト自身じゃなくて、Compareメソッドを提供して、2つのオブジェクトの順序を決めるだけ。
シンタックス
IComparer<T>インターフェースのシンタックスはめっちゃシンプル:
public interface IComparer<T>
{
// T型の2つのオブジェクトを比較するメソッド
// x - 比較する1つ目のオブジェクト
// y - 比較する2つ目のオブジェクト
int Compare(T x, T y);
}
Compare(T x, T y)メソッドは、IComparable<T>のCompareTo(T other)と全く同じ動き:
- xがyより「小さい」なら負の数を返す。
- xがyと「等しい」ならゼロ (0)を返す。
- xがyより「大きい」なら正の数を返す。
ここでの「小さい」「等しい」「大きい」は、Compareの実装で自分が決めたロジック次第だよ。
IComparer<T>とIComparable<T>の違い
| クラス / インターフェース | どこで実装するか | 用途 | 使用例 |
|---|---|---|---|
|
型(クラス/構造体)自体に | 1つの標準的な比較方法 | IDの昇順ソート |
|
別クラスで | 好きなだけ比較方法を作れる | 名前順、日付順ソート |
3. IComparer<T>の実践例
じゃあ「1つの大きなアプリ」ってことで、シンプルなユーザーモデルを作ってみよう。こんなクラスがあるとする:
// ユーザークラス
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
名前順ソート:専用コンパレーターを作る
IComparer<User>を実装した、名前でユーザーを比べるクラスを書いてみる:
// 名前順ソート用コンパレーター
public class UserNameComparer : IComparer<User>
{
public int Compare(User x, User y)
{
// nullチェック(意外なバグ防止!)
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1; // nullはどんなオブジェクトより「小さい」
if (y is null) return 1;
// 名前で比較(標準の文字列ソートルールで)
return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
}
}
コンパレーターでリストをソートしてみる:
List<User> users = new List<User>
{
new User { Name = "イワン", Age = 20, Email = "ivan@mail.com" },
new User { Name = "アンナ", Age = 32, Email = "anna@gmail.com" },
new User { Name = "ボリス", Age = 28, Email = "boris@work.org" },
new User { Name = "ルスラン", Age = 19, Email = "ruslan@yandex.ru" }
};
// IComparerで名前順ソート
users.Sort(new UserNameComparer());
users.ForEach(u => Console.WriteLine(u.Name)); // アンナ, ボリス, イワン, ルスラン
ね、めっちゃキレイでスマートでしょ?コンパレータークラスはプログラムの独立した市民みたいなもので、他のユーザーリストにも再利用できる。
4. 比較方法いろいろ:コンパレーターを複数作る
好きなだけコンパレーターを作れるのが面白いところ。例えば年齢順ソートを作ってみよう:
// 年齢順ソート用コンパレーター
public class UserAgeComparer : IComparer<User>
{
public int Compare(User x, User y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
// 年齢の昇順でソート
return x.Age.CompareTo(y.Age);
}
}
で、こう使う:
users.Sort(new UserAgeComparer());
users.ForEach(u => Console.WriteLine($"{u.Name} ({u.Age})"));
// 出力: ルスラン (19) イワン (20) ボリス (28) アンナ (32)
年齢の降順で並べたいなら、引数の順番を逆にするだけ:
// 年齢降順ソート用コンパレーター
public class UserAgeDescendingComparer : IComparer<User>
{
public int Compare(User x, User y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
// 順番逆に: y.CompareTo(x)
return y.Age.CompareTo(x.Age);
}
}
5. 便利なポイント
中でどう動いてるの?
Sort()にコンパレーターを渡すと、リストは各要素をコンパレーターに投げて「どっちを先にする?」って聞く。Compareが「こっち、こっち、または順番そのままでOK」と答える。それを全部のペアで繰り返して、最終的なソート済みリストができる。
値が等しい場合は?0を返すだけ。順番はそのまま(またはソートの内部ロジック次第)。
IComparer<T>は他にどこで使われてる?
IComparer<T>はリスト以外でも.NETでよく使われてる。例えば:
- SortedList<TKey, TValue>やSortedSet<T>みたいなコレクションのコンストラクタ:要素の順序を指定するため。
- BinarySearchでの検索。
例えばこんな感じ:
var sortedSet = new SortedSet<User>(new UserAgeComparer());
これでSortedSetは常に年齢順を自動で保ってくれる!
nullセーフについてちょっとだけ
初心者がよくやるミスの一つがNullReferenceException。Compareの中でnullチェックを忘れずに。リストにnullが入ってるかもしれないからね。
よく使うパターン(もう一回書いとくね、念のため):
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
これを習慣にしとくと、プログラムが変なタイミングで落ちるのを防げるよ!
IComparer<T>アプローチのメリット・デメリット
- 比較ロジックとデータをしっかり分離できる。ユーザークラスは「どうソートされるか」を気にしなくていい。
- 比較ロジックをいろんな場所で簡単に再利用できる。
- 拡張性バツグン:元の型を変えずに好きなだけソート方法を増やせる。
でも「null忘れた」とか「比較ロジックが一貫してない」みたいなミスには注意。例えばCompare(x, y)が0を返したら、Compare(y, x)も0じゃないとダメ。Compare(x, y)が0より大きいなら、Compare(y, x)は0より小さくないとダメ、みたいな。
ビジュアル早見表:どれを使う?
| やりたいこと | 使うもの | ロジックの場所 |
|---|---|---|
| 「自然な」ソートが1つだけ | |
型(クラス/構造体)自体 |
| いろんなソート方法 | |
別クラス |
| サクッと一回だけ、「その場で」 | / ラムダ |
Sortメソッドの引数(デリゲートで) |
| 複雑でよく使うロジック | |
専用のコンパレータークラス |
次のレクチャーでは、デリゲートやラムダ式を使ってオブジェクトを比較・組み合わせる方法を紹介するよ。まずは自分のアプリでいろんなコンパレーターを作ってみて、ソートロジックがメインクラスの外にあるエレガントな設計を楽しんでみて!
6. コンパレーター実装時のよくあるミス
ミス1:nullチェックを忘れる。
比較するオブジェクトのどっちかがnullで、コードでそれを考慮してないと、NullReferenceExceptionで落ちるよ。
ミス2:-1、0、+1の値が変。
Compareメソッドは、1つ目が2つ目より小さいなら負の数、等しいなら0、大きいなら正の数を返さないとダメ。これを守らないとソートが「変な動き」になる。
ミス3:比較ロジックが非対称。
xとyを比べて返す値と、yとxを比べて返す値が逆じゃないと、結果が予測不能になる。
ミス4:ユーザー定義型でコンパレーターなしでSort()を使う。
型がIComparableやIComparable<T>を実装してないのに、コンパレーターなしでSort()を呼ぶと、InvalidOperationExceptionで落ちる。
どう防ぐ?
境界ケースをちゃんとチェックして、重要な部分はユニットテストでカバー(これもまた今度やるよ!)、ドキュメントもどんどん見てOK。コンパレーターがスイス時計みたいに動くようにしよう!
GO TO FULL VERSION