コレクションをソートしたり、2 つのオブジェクトの大小を比較したりするとき、C# では IComparable と IComparer という 2 つのインターフェースが中心的な役割を担います。
この記事では両インターフェースの役割と実装方法を整理し、複数のソート順を扱う IComparer の活用パターン、カスタムプロパティに応じたソート型の設計まで実例とともに解説します。
IComparable — 自然順序を定義する
IComparable<T> は、型自身が「自分とほかのインスタンスを比較する方法」を知っているという契約です。System 名前空間に定義されています。
public interface IComparable<T>
{
int CompareTo(T? other);
}
非ジェネリック版の IComparable もありますが、型安全性のためジェネリック版を使うのが現代の標準です。
CompareTo の戻り値の意味
CompareTo は int を返します。この値が何を意味するかは次のとおりです。
| 戻り値 | 意味 |
|---|---|
| 負の値(< 0) | this は other より小さい |
| 0 | this は other と等しい |
| 正の値(> 0) | this は other より大きい |
Array.Sort、List<T>.Sort、LINQ の OrderBy などはすべてこの戻り値を利用して要素を並べ替えます。
基本的な実装例
気温を表すシンプルなクラスで実装します。
public class Temperature : IComparable<Temperature>
{
public double Celsius { get; }
public Temperature(double celsius) => Celsius = celsius;
public int CompareTo(Temperature? other)
{
if (other is null) return 1; // null は自分より小さいと見なす
return Celsius.CompareTo(other.Celsius); // double の CompareTo に委譲
}
public override string ToString() => $"{Celsius:F1}°C";
}
var temps = new[]
{
new Temperature(36.5),
new Temperature(38.0),
new Temperature(37.2),
new Temperature(35.8),
};
Array.Sort(temps);
foreach (var t in temps)
Console.WriteLine(t);
// 35.8°C
// 36.5°C
// 37.2°C
// 38.0°C
IComparable<T> を実装した型は Array.Sort や List<T>.Sort を引数なしで呼び出せるようになります。これが「自然順序(natural order)」です。
CompareTo の null 処理
IComparable<T> の規約では、null は自分より小さいと見なすのが慣例です(null.CompareTo(anything) が NullReferenceException にならないよう、null を渡した側が 1 を受け取ることになります)。
public int CompareTo(Temperature? other)
{
if (other is null) return 1; // this > null
return Celsius.CompareTo(other.Celsius);
}
複合フィールドによる比較
複数のフィールドを使って順序を定義することもよくあります。まず優先度の高いフィールドで比較し、等しければ次のフィールドで比較します。
public class Employee : IComparable<Employee>
{
public string Department { get; }
public string Name { get; }
public decimal Salary { get; }
public Employee(string department, string name, decimal salary)
{
Department = department;
Name = name;
Salary = salary;
}
// 部署 → 名前 の優先順で比較
public int CompareTo(Employee? other)
{
if (other is null) return 1;
int deptComp = string.Compare(Department, other.Department, StringComparison.Ordinal);
if (deptComp != 0) return deptComp;
return string.Compare(Name, other.Name, StringComparison.Ordinal);
}
public override string ToString() => $"{Department} / {Name} ({Salary:C})";
}
var employees = new List<Employee>
{
new("営業", "鈴木", 400_000),
new("開発", "山田", 450_000),
new("営業", "田中", 380_000),
new("開発", "佐藤", 420_000),
};
employees.Sort();
employees.ForEach(Console.WriteLine);
// 営業 / 田中 (¥380,000)
// 営業 / 鈴木 (¥400,000)
// 開発 / 佐藤 (¥420,000)
// 開発 / 山田 (¥450,000)
LINQ の OrderBy との連携
IComparable<T> を実装していれば、LINQ の OrderBy() / OrderByDescending() でもそのまま使えます。
var sorted = employees.OrderBy(e => e).ToList(); // 自然順序
var desc = employees.OrderByDescending(e => e).ToList(); // 逆順
プロパティを直接指定することもできます。
var bySalary = employees.OrderBy(e => e.Salary);
var byName = employees.OrderBy(e => e.Name);
IComparer<T> — 外部から比較ロジックを注入する
IComparable<T> が型自身に順序を埋め込むのに対し、IComparer<T> は比較ロジックを型の外部に切り出すインターフェースです。
public interface IComparer<T>
{
int Compare(T? x, T? y);
}
Array.Sort(array, comparer) や List<T>.Sort(comparer) のようにソートメソッドの引数として渡すことで、自然順序以外の並び順を指定できます。
自然順序とは別のソートが必要なとき
Employee を「給与の降順」でソートしたい場合、IComparable の自然順序(部署→名前)は使えません。IComparer<Employee> を実装した専用クラスを用意します。
public class SalaryDescendingComparer : IComparer<Employee>
{
public int Compare(Employee? x, Employee? y)
{
if (x is null && y is null) return 0;
if (x is null) return -1;
if (y is null) return 1;
// 降順なので y と x の順序を逆にする
return y.Salary.CompareTo(x.Salary);
}
}
employees.Sort(new SalaryDescendingComparer());
employees.ForEach(Console.WriteLine);
// 開発 / 山田 (¥450,000)
// 開発 / 佐藤 (¥420,000)
// 営業 / 鈴木 (¥400,000)
// 営業 / 田中 (¥380,000)
複数のソート順を指定する(Specifying Multiple Sort Orders)
実際のアプリケーションでは「名前順」「給与順」「部署順」など複数のソート基準が必要になります。それぞれに IComparer<T> を実装することで、用途に応じたソートを柔軟に切り替えられます。
// 名前の昇順
public class NameAscendingComparer : IComparer<Employee>
{
public int Compare(Employee? x, Employee? y)
{
if (x is null && y is null) return 0;
if (x is null) return -1;
if (y is null) return 1;
return string.Compare(x.Name, y.Name, StringComparison.CurrentCulture);
}
}
// 給与の昇順
public class SalaryAscendingComparer : IComparer<Employee>
{
public int Compare(Employee? x, Employee? y)
{
if (x is null && y is null) return 0;
if (x is null) return -1;
if (y is null) return 1;
return x.Salary.CompareTo(y.Salary);
}
}
// 部署の昇順、同部署なら給与の降順
public class DepartmentThenSalaryComparer : IComparer<Employee>
{
public int Compare(Employee? x, Employee? y)
{
if (x is null && y is null) return 0;
if (x is null) return -1;
if (y is null) return 1;
int deptComp = string.Compare(x.Department, y.Department, StringComparison.CurrentCulture);
if (deptComp != 0) return deptComp;
return y.Salary.CompareTo(x.Salary); // 同部署なら給与降順
}
}
// 名前順
employees.Sort(new NameAscendingComparer());
Console.WriteLine("-- 名前順 --");
employees.ForEach(e => Console.WriteLine(e.Name));
// 佐藤 / 鈴木 / 田中 / 山田
// 部署 → 給与降順
employees.Sort(new DepartmentThenSalaryComparer());
Console.WriteLine("-- 部署 → 給与降順 --");
employees.ForEach(Console.WriteLine);
// 営業 / 鈴木 (¥400,000)
// 営業 / 田中 (¥380,000)
// 開発 / 山田 (¥450,000)
// 開発 / 佐藤 (¥420,000)
Comparer<T>.Create で簡潔に書く
毎回クラスを定義するのが煩雑な場合は、Comparer<T>.Create にラムダ式を渡すと一行で済みます。
// 給与の降順
var bySalaryDesc = Comparer<Employee>.Create((x, y) => y!.Salary.CompareTo(x!.Salary));
employees.Sort(bySalaryDesc);
// 名前の昇順
employees.Sort(Comparer<Employee>.Create((x, y) =>
string.Compare(x!.Name, y!.Name, StringComparison.CurrentCulture)));
Comparison<T> デリゲートを直接使う
List<T>.Sort はラムダ式を Comparison<T> デリゲートとして直接受け取るオーバーロードも持っています。最も手軽な方法です。
// 給与の昇順(ラムダで直接指定)
employees.Sort((x, y) => x.Salary.CompareTo(y.Salary));
// 名前の降順
employees.Sort((x, y) => string.Compare(y.Name, x.Name, StringComparison.CurrentCulture));
LINQ の OrderBy + ThenBy による複合ソート
LINQ ではメソッドチェーンで複合ソートを表現できます。独自の IComparer<T> を渡すこともできます。
// 部署昇順 → 給与降順
var result = employees
.OrderBy(e => e.Department)
.ThenByDescending(e => e.Salary);
foreach (var e in result)
Console.WriteLine(e);
// IComparer を使う場合
var result2 = employees.OrderBy(e => e, new DepartmentThenSalaryComparer());
カスタムプロパティとカスタムソート型(Custom Properties and Custom Sort Types)
UI やビジネスロジックでは「ユーザーが選んだ列でソートする」といった動的なソートが必要になります。ここでは、ソート基準をカプセル化した専用の型を設計するパターンを紹介します。
ソートキーを列挙型で表現する
public enum EmployeeSortKey
{
Name,
Department,
Salary,
}
public enum SortDirection
{
Ascending,
Descending,
}
汎用的な IComparer<T> を組み立てるファクトリ
public static class EmployeeComparerFactory
{
public static IComparer<Employee> Create(EmployeeSortKey key, SortDirection direction)
{
Comparison<Employee> comparison = key switch
{
EmployeeSortKey.Name =>
(x, y) => string.Compare(x.Name, y.Name, StringComparison.CurrentCulture),
EmployeeSortKey.Department =>
(x, y) => string.Compare(x.Department, y.Department, StringComparison.CurrentCulture),
EmployeeSortKey.Salary =>
(x, y) => x.Salary.CompareTo(y.Salary),
_ => throw new ArgumentOutOfRangeException(nameof(key)),
};
if (direction == SortDirection.Descending)
{
var original = comparison;
comparison = (x, y) => -original(x, y); // 符号反転で逆順
}
return Comparer<Employee>.Create(comparison);
}
}
// 実行時にソートキーを切り替えられる
var key = EmployeeSortKey.Salary;
var dir = SortDirection.Descending;
var comparer = EmployeeComparerFactory.Create(key, dir);
employees.Sort(comparer);
employees.ForEach(Console.WriteLine);
// 開発 / 山田 (¥450,000)
// 開発 / 佐藤 (¥420,000)
// 営業 / 鈴木 (¥400,000)
// 営業 / 田中 (¥380,000)
ソート条件を複数組み合わせる汎用コンポジット
複数のソートキーを連結する汎用 IComparer<T> を実装することで、LINQ の ThenBy と同等のことを非 LINQ 環境(Array.Sort など)でも実現できます。
public class CompositeComparer<T> : IComparer<T>
{
private readonly IReadOnlyList<IComparer<T>> _comparers;
public CompositeComparer(params IComparer<T>[] comparers)
{
_comparers = comparers;
}
public int Compare(T? x, T? y)
{
foreach (var comparer in _comparers)
{
int result = comparer.Compare(x, y);
if (result != 0) return result;
}
return 0;
}
}
// 部署昇順 → 給与降順 の組み合わせ
var comparer = new CompositeComparer<Employee>(
EmployeeComparerFactory.Create(EmployeeSortKey.Department, SortDirection.Ascending),
EmployeeComparerFactory.Create(EmployeeSortKey.Salary, SortDirection.Descending)
);
employees.Sort(comparer);
employees.ForEach(Console.WriteLine);
// 営業 / 鈴木 (¥400,000)
// 営業 / 田中 (¥380,000)
// 開発 / 山田 (¥450,000)
// 開発 / 佐藤 (¥420,000)
SortedSet<T> や SortedDictionary<TKey,TValue> への応用
IComparer<T> はコレクションのコンストラクタにも渡せます。要素の挿入時点から自動的に指定した順序で管理されます。
// 給与の降順で自動ソートする SortedSet
var sortedByGross = new SortedSet<Employee>(
Comparer<Employee>.Create((x, y) => y.Salary.CompareTo(x.Salary))
);
sortedByGross.Add(new Employee("開発", "山田", 450_000));
sortedByGross.Add(new Employee("営業", "田中", 380_000));
sortedByGross.Add(new Employee("開発", "佐藤", 420_000));
foreach (var e in sortedByGross)
Console.WriteLine(e);
// 開発 / 山田 (¥450,000)
// 開発 / 佐藤 (¥420,000)
// 営業 / 田中 (¥380,000)
IComparable と IComparer の使い分け
IComparable<T> |
IComparer<T> |
|
|---|---|---|
| 誰が比較ロジックを持つか | 型自身 | 外部クラス |
| 用途 | 自然順序(1 種類) | 複数ソート戦略・外部から注入 |
| 実装場所 | 比較される型に CompareTo を書く |
専用の比較クラスを別途作る |
Array.Sort への渡し方 |
引数なし | 第 2 引数に IComparer<T> を渡す |
| LINQ との組み合わせ | OrderBy(e => e) |
OrderBy(e => e, comparer) |
実用上は両方を組み合わせるのが一般的です。最もよく使われる順序を IComparable<T> として型に定義し、その他の順序は IComparer<T> で外付けするというスタイルです。
まとめ
IComparable<T>は型自身に「自然順序」を与える契約。Array.Sortや LINQ が無条件でこれを利用するCompareToの戻り値は「負 = 小さい、0 = 等しい、正 = 大きい」- 複合フィールドの比較は優先順位が高いフィールドから順に評価する
IComparer<T>は比較ロジックを外部化する。複数のソート順を切り替えたい場面で使うComparer<T>.CreateやComparison<T>デリゲートで手軽に比較ロジックを作れる- ソートキーを列挙型 + ファクトリで表現すると、動的なソートに対応した柔軟な設計になる
CompositeComparer<T>を使うと複数ソートキーの連結をArray.Sortでも実現できる