bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] IComparable と IComparer — オブジェクトの順序比較と複数ソート戦略

[C#] IComparable と IComparer — オブジェクトの順序比較と複数ソート戦略

May 9, 2026 C# , .NET bucket-sort

コレクションをソートしたり、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 でも実現できる
C# .NET IComparable IComparer CompareTo ソート 比較 オブジェクト指向
← [C#] ICloneable インターフェース — オブジェクトのコピーと浅いコピー・深いコピー [C#] System.GC クラスを整理する — ガベージコレクションを制御するための API →

Related Posts

  • [C#] ICloneable インターフェース — オブジェクトのコピーと浅いコピー・深いコピー May 8, 2026
  • [C#] インターフェースの活用パターン — 参照取得・パラメータ・配列・既定実装・階層設計 May 6, 2026
  • [C#] インターフェース(Interface)を体系的に理解する May 5, 2026
  • [C#] Finalizable & Disposable パターン実践 — Dispose パターンの完全形 May 13, 2026

Table of Contents

  • IComparable — 自然順序を定義する
    • CompareTo の戻り値の意味
    • 基本的な実装例
    • CompareTo の null 処理
    • 複合フィールドによる比較
    • LINQ の OrderBy との連携
  • IComparer<T> — 外部から比較ロジックを注入する
    • 自然順序とは別のソートが必要なとき
  • 複数のソート順を指定する(Specifying Multiple Sort Orders)
    • Comparer<T>.Create で簡潔に書く
    • Comparison<T> デリゲートを直接使う
    • LINQ の OrderBy + ThenBy による複合ソート
  • カスタムプロパティとカスタムソート型(Custom Properties and Custom Sort Types)
    • ソートキーを列挙型で表現する
    • 汎用的な IComparer<T> を組み立てるファクトリ
    • ソート条件を複数組み合わせる汎用コンポジット
    • SortedSet<T> や SortedDictionary<TKey,TValue> への応用
  • IComparable と IComparer の使い分け
  • まとめ

Recent Posts

  • [C#] Finalizable & Disposable パターン実践 — Dispose パターンの完全形 May 13, 2026
  • [C#] Disposable Objects — IDisposable / Dispose() と using 構文 May 12, 2026
  • [C#] Finalizable Objects — Finalize() の役割と使いどころ May 11, 2026
  • [C#] System.GC クラスを整理する — ガベージコレクションを制御するための API May 10, 2026
  • [C#] IComparable と IComparer — オブジェクトの順序比較と複数ソート戦略 May 9, 2026

Categories

  • C#63
  • .NET62
  • AWS27
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Filament1
  • Git1
  • SQS1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • Apache
  • Code Snippet
  • DynamoDB
  • NoSQL
  • PHP-FPM
  • RDS
  • パフォーマンス
  • DoS
  • Nginx
  • Windows
  • WordPress
  • メモリ管理
  • 監視
  • 設計
  • Amazon Linux 2023
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • OPCache
  • Webサーバー
  • オブジェクト指向
  • クラス設計
  • コレクション
  • デザインパターン
  • パターンマッチング
  • 継承
  • 認可
  • Aurora
  • Blade
  • Grafana
  • Hugo
  • InfluxDB
  • Policy
  • Record
  • SSG
  • インターフェース
  • エラーハンドリング
  • カプセル化
  • ガベージコレクション
  • モニタリング
  • 例外
Powered by Hugo & Explore Theme.