CodeGym /コース /C# SELF /.NETでのコンパレータの実践的な使い方

.NETでのコンパレータの実践的な使い方

C# SELF
レベル 30 , レッスン 4
使用可能

1. はじめに

なぜコンパレータは本当に大事なのか…

実際のプロジェクトだと、教材の課題みたいに単純じゃないんだよね。オブジェクトは複雑だし、データはユーザーや他サービスから来るし、必要な「ソート順」や「等価のルール」もタスクごとにコロコロ変わる。コンパレータを使えば、コードが柔軟になって、動作も予測しやすくコントロールできるんだ。

.NETでは、ユーザー定義クラスのオブジェクトをソート、検索、グループ化、重複排除したいとき、また「順序付き」データ構造を作るときにコンパレータが必須。特にコレクションやアルゴリズム、外部APIとの連携でよく出てくるよ。

.NETでコンパレータが登場する場面:

  • コレクションのソート(List<T>.SortArray.SortOrderBy in LINQ)
  • 順序付きデータ構造(SortedSet<T>SortedDictionary<TKey, TValue>
  • コレクション内のオブジェクトの検索・比較(ContainsIndexOf — 「等価」だけじゃなくて順序も必要なとき)
  • グループ化、フィルタ、重複排除(例:.Distinct()

実際に何のために必要?

  • ソートされた出力が大事なレポート(例:学生リストを名前順や平均点順で)
  • 入力データを基準値と照合(例:特定条件でレコード検索)
  • メモリや速度の節約(適切なデータ構造を選ぶと検索が速くなってアプリがサクサク動く)
  • ユニーク性のバリデーション(例:ユーザー登録時のe-mail重複チェック)

2. 実例:いろんな基準でソートしてみる

例えば、こんなユーザークラスがあるとする:

public class User
{
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";
    public string Email { get; set; } = "";
    public int Age { get; set; }

    // override EqualsやGetHashCodeも追加できるけど、それは宿題ね
}

ユーザーのリストがあって、やりたいことは:

  • 苗字でソート、苗字が同じなら名前でソートしたい。
  • e-mailでユーザー検索(大文字小文字は無視)。
  • ユーザー追加時に重複を排除したい。

IComparer<T>でソートする

その1:クラシックなやり方は、専用のコンパレータを作ること。

// 苗字+名前でソートするコンパレータ
public class UserFullNameComparer : 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;

        int lastNameComparison = StringComparer.OrdinalIgnoreCase.Compare(x.LastName, y.LastName);
        if (lastNameComparison != 0)
            return lastNameComparison;

        return StringComparer.OrdinalIgnoreCase.Compare(x.FirstName, y.FirstName);
    }
}

使い方:

var users = new List<User>
{
    new User { FirstName = "イワン", LastName = "ペトロフ", Age = 20, Email = "ivan.petrov@email.com" },
    new User { FirstName = "アンナ", LastName = "スミルノワ", Age = 22, Email = "anna.smirnova@email.com" },
    new User { FirstName = "ピョートル", LastName = "ペトロフ", Age = 18, Email = "petr.petrov@email.com" }
};

users.Sort(new UserFullNameComparer());

foreach (var user in users)
{
    Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");
}

// 結果:ユーザーは苗字で、同じ苗字なら名前でソートされる。

メモ: 比較順序がアプリのいろんな場所で必要なときや、ラムダじゃコピペ地獄になるときはこうやるのが定番。

ラムダで「その場ソート」

クラス増やしたくない?一回きりの順序ならラムダでOK!

users.Sort((x, y) =>
{
    int lastNameComparison = StringComparer.OrdinalIgnoreCase.Compare(x.LastName, y.LastName);
    if (lastNameComparison != 0)
        return lastNameComparison;
    return StringComparer.OrdinalIgnoreCase.Compare(x.FirstName, y.FirstName);
});

動きは同じだけど、コンパレータをその場で作るからコードが短くなる。ただし何度も使うならちょっと不便かも。

3. ちょっと工夫:e-mailを大文字小文字無視で比較

現実だとユーザーはe-mailを好き勝手に入力するし、君はプログラマーであって裁判官じゃないから、e-mailは大文字小文字無視で比較するのが普通だよね。

コンパレータと検索で実装してみよう:

public class EmailComparer : IEqualityComparer<User>
{
    public bool Equals(User? x, User? y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x is null || y is null) return false;
        return string.Equals(x.Email, y.Email, StringComparison.OrdinalIgnoreCase);
    }
    public int GetHashCode(User obj)
    {
        return obj.Email?.ToLowerInvariant().GetHashCode() ?? 0;
    }
}

HashSetでの使い方:

var usersSet = new HashSet<User>(new EmailComparer());
usersSet.Add(new User { Email = "Petrov@example.com" });
bool contains = usersSet.Contains(new User { Email = "petrov@example.com" }); // true!

重要ポイント: 独自のEqualityComparerを作るときは、EqualsGetHashCodeの両方を必ず実装しよう。片方だけだと、コレクションでの検索が変な動きになるよ。

4. SortedSetSortedDictionaryの使いどころ

ここでコンパレータの本領発揮!

SortedSet<T>SortedDictionary<TKey, TValue>は、.NETに「どう比較するか」を教えないと君のオブジェクトを扱えない。しかも比較や等価のルール次第で、どれが別物扱いになるかも変わる!

SortedSet<User>の例

var sortedUsersByFullName = new SortedSet<User>(new UserFullNameComparer())
{
    new User { FirstName = "イワン", LastName = "ペトロフ", Age = 20 },
    new User { FirstName = "アンナ", LastName = "スミルノワ", Age = 22 },
    new User { FirstName = "ピョートル", LastName = "ペトロフ", Age = 18 },
    new User { FirstName = "イワン", LastName = "ペトロフ", Age = 25 } // 名前と苗字が同じのダブり
};

// 年齢が違う「イワン ペトロフ」は1人だけセットに入る
Console.WriteLine("SortedSetのユーザー一覧:");
foreach (var user in sortedUsersByFullName)
    Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");

SortedSetは「イワン ペトロフ」を2回入れても1人しか入らない!

注意: ここでのコンパレータが「ユニーク性」のロジックを決める。苗字+名前だけで比較すると、年齢が違っても同じ名前なら同一人物扱いになるよ。

5. .NETでどのコンパレータを使うか早見表

シナリオ 何を実装するか 使い方例
自然なソート順(型にとって1つだけの標準順序)
IComparable<T>
List<T>.Sort()
いろんなソート順(名前順、年齢順、e-mail順…)
IComparer<T>
(クラスやラムダ)
List<T>.Sort(comparer)
コレクション内のユニーク要素検索
IEqualityComparer<T>
HashSet<T>
,
Dictionary<TKey, TValue>
グループ化、重複削除
IEqualityComparer<T>
LINQ
.Distinct(comparer)
時間や日付、複雑なフィールドの組み合わせでソート
Comparison<T>
、その場ラムダ
List<T>.Sort((a, b) => ...)
LINQで使う(一回きりのクエリ)
OrderBy
ThenBy
のラムダ
OrderBy(x => x.Name)

6. 実践:コレクションでの検索と比較

アプリで、e-mail(大文字小文字無視)がユニークなユーザー登録を実装してみよう。同じアドレスが既にあれば、それを伝える必要がある。

public static bool RegisterUser(List<User> users, User newUser)
{
    // e-mailのユニーク性チェックはAny+ラムダで
    bool exists = users.Any(u =>
        string.Equals(u.Email, newUser.Email, StringComparison.OrdinalIgnoreCase));
    if (exists)
    {
        Console.WriteLine($"ユーザーのe-mail: {newUser.Email}は既に登録済みだよ!");
        return false;
    }
    users.Add(newUser);
    Console.WriteLine($"ユーザー{newUser.FirstName}を追加したよ。");
    return true;
}

使い方:

var userList = new List<User>
{
    new User { Email = "first@example.com" }
};

RegisterUser(userList, new User { FirstName = "ヴァーシャ", Email = "FIRST@example.com" });
// 出力: "ユーザーのe-mail: FIRST@example.comは既に登録済みだよ!"

7. コンパレータでありがちなミス

みんなエラーリスト好きだけど、ここはちょっとストーリー仕立てで。

プログラマーがユーザー定義型のコンパレータを実装するとき、nullチェックを忘れたり、もっとヤバいのは「順序の厳密性」を壊すような比較を書いちゃうことがある。例えば、矛盾した値を返すと、ソートが変な動きになったり、コレクションでオブジェクトが消えたり「くっついたり」する。

もう一つよくあるのは、Equalsの実装とコンパレータのロジックが食い違うこと。例えば、Equalsは違うと判定するのに、コンパレータは同じと判定すると、SortedSetSortedDictionaryでカオスが発生。要素が見つからないのに「あるはず」みたいなことになる。

あと、オブジェクトの特徴(例えば苗字)だけで比較して、同じ苗字の他ユーザーがいることを忘れるパターンも多い。その結果、オブジェクトが「上書き」されたり消えたりして、データが一貫性を失う。つまり、システムの本当の状態を反映しなくなって、重複や情報消失、プログラムのロジック崩壊につながるんだ。

1
アンケート/クイズ
コンパレーター、レベル 30、レッスン 4
使用不可
コンパレーター
コンパレーターとオブジェクトの比較
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION