CodeGym /コース /C# SELF /インターフェース IComparable<T&...

インターフェース IComparable<T>

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

1. はじめに

友達に本を棚に並べてって頼んだと想像してみて。もし背表紙に番号が書いてある本(1, 2, 3…)なら、友達は迷わず並べられるよね。「あ、12の前、23の前だな」って感じ。これが自然な順序(数字の順番)だよ。同じように、タイトルが「A」「B」「C」から始まる本なら、アルファベット順に並べる。これも文字列の「自然な」順序(アルファベット順)だね。

でも、もし背表紙に著者の名字だけが書いてある本だったら?「順番に並べて」って頼んだら、友達は「どの順番?名字?出版年?ページ数?」って聞いてくるはず。ここで問題発生。君のユニークなオブジェクトには明らかな自然な順序がないんだ。

プログラミングでも同じことが起きる。intのリスト List<int> をソートしようとしたら、C#はどうすればいいか分かってる。List<string> も、辞書順(アルファベット順)でOK。でも List<Student> みたいに、Student が名前・名字・年齢・IDとか色々持ってると、C#は迷子になる。どの項目で比較すればいいの?名前?ID?平均点?これが「コレクションのソート」レクチャーで出てきた問題で、List<T>.Sort() を使ったらカスタム型のコレクションでエラーが出たやつだね。

このパズルを解くには、C#に「どの基準で比較するか」をちゃんと教えてあげる必要がある。そのために IComparable<T> インターフェースがあるんだ。

2. インターフェース IComparable<T>

つまり、自分のオブジェクトに「他の同じ型のオブジェクトと比べて自分の位置を知る」能力を持たせたいなら、IComparable<T> インターフェースを使う。これは契約みたいなもの。クラスや構造体がこのインターフェースを実装すると、「よっ!俺のオブジェクトは比較できるぜ。やり方はこれだ!」ってコンパイラに伝える感じ。

どうやって動くの?

IComparable<T> インターフェースは、たった1つのメソッドを定義してる:

public interface IComparable<in T>
{
    int CompareTo(T other);
}

このメソッドは T 型(今のオブジェクトと同じ型)のオブジェクトを受け取って、次の値を返さなきゃいけない:

  • 負の数(< 0)なら、今のオブジェクトが「小さい」ってこと;
  • ゼロ(0)なら、(ソート的に)「等しい」;
  • 正の数(> 0)なら、今のオブジェクトが「大きい」。

要は、ボクシングの審判みたいなもん。君が強かったらラウンド勝ちで点数多い、弱かったら少ない、互角なら同点。でもここではもっとシンプルで、符号付きの数字だけ。

なんでこうなってるの?

ソート系のメソッド(たとえば List<T>.Sort())は、リストの要素に対して CompareTo を呼び出して、どっちを先に置くか決めてる。君のクラスがこのインターフェースを実装してれば、ソートできるってわけ!

3. 実践

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

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

ユーザーのリストをソートしてみよう:

List<User> users = new List<User>
{
    new User { Name = "セルゲイ", Age = 31 },
    new User { Name = "マリヤ", Age = 22 },
    new User { Name = "アントン", Age = 27 }
};

users.Sort(); // ドカン!InvalidOperationException

エラーが出る:"At least one object must implement IComparable"(最低でも1つのオブジェクトがIComparableを実装してなきゃダメ)。

修正:IComparable<User> を実装しよう

クラスにインターフェースを追加しよう。まずは年齢順(若い順)でソートしてみる:

public class User : IComparable<User>
{
    public string Name { get; set; }
    public int Age { get; set; }

    public int CompareTo(User other)
    {
        // おバカ対策:other == null なら自分が「大きい」
        if (other == null) return 1;
        return this.Age.CompareTo(other.Age); // 年齢でソート
    }
}

リストを作って users.Sort(); を呼び出して、結果をコンソールに出してみよう:

List<User> users = new List<User>
{
    new User { Name = "セルゲイ", Age = 31 },
    new User { Name = "マリヤ", Age = 22 },
    new User { Name = "アントン", Age = 27 }
};

// 年齢でソート(CompareToを使う)
users.Sort();

// ソート後のユーザーを出力
foreach (User user in users)
{
    Console.WriteLine($"{user.Name}, {user.Age}");
}

リストは年齢順に並ぶよ:

マリヤ, 22
アントン, 27
セルゲイ, 31

ビジュアル化:ソート前と後

名前 年齢
セルゲイ 31
マリヤ 22
アントン 27

ソート後:

名前 年齢
マリヤ 22
アントン 27
セルゲイ 31

4. 大事なポイントとよくあるミス

null 対策

CompareTo の中で、othernull じゃないか必ずチェックしよう。これを忘れると NullReferenceException が出る。これはリリース直前のバグ並みに厄介。普通は、比較対象が null なら自分が「大きい」とみなすのが定番:

public int CompareTo(User other)
{
    if (other == null) return 1;
    // ...
}

推移律を守ろう

A < B かつ B < C なら、A < C じゃなきゃダメ。これを守らないと、ソートがカオスになる(面白いけど間違い!)。

複数フィールドでソートしたい場合

たとえば、まず年齢でソートして、同い年なら名前のアルファベット順で並べたい場合はこうする:

public int CompareTo(User other)
{
    if (other == null) return 1;

    int ageCompare = this.Age.CompareTo(other.Age);
    if (ageCompare != 0) return ageCompare;

    // 年齢が同じなら名前で比較
    return this.Name.CompareTo(other.Name);
}

5. ソート:君のオブジェクトもOK

List<int> ができることは、君のクラスもできる

これで、比較が必要なメソッドは全部使える:SortBinarySearch、ソート済みコレクション(たとえば SortedSet<T>)への追加もOK。

users.Sort();
// usersは年齢(同じなら名前)でソート済み

コースアプリの文脈での例

前にユーザー管理アプリを作ったことがあるとしよう。今なら「自然な」ソート順を既存コードに追加できる。こんな感じ:

// UserクラスはすでにIComparable<User>を実装済み

List<User> users = new List<User>
{
    new User { Name = "イワン", Age = 45 },
    new User { Name = "ガリナ", Age = 27 },
    new User { Name = "ユーリ", Age = 27 }
};

// 年齢→名前の順でソート
users.Sort();
foreach (var u in users)
{
    Console.WriteLine($"{u.Name} - {u.Age}");
}

結果:

ガリナ - 27
ユーリ - 27
イワン - 45

ガリナとユーリは同い年なので、名前順で並ぶ。

6. CompareTo の仕組み:ガチな数学

もう一度、返り値のルールを強調しとくね:

  • 負の数(例:-1):今のオブジェクトが比較対象より前。
  • ゼロ:ソート的に等しい。
  • 正の数(例:1):今のオブジェクトが比較対象より後。

組み込み型(たとえば Age.CompareTo(other.Age))はこのルールを守ってて、必ず -101 を返すよ。

CompareTo メソッドの返り値テーブル

返り値 意味
< 0 小さい(前に来る)
20.CompareTo(21): -1
0 等しい
23.CompareTo(23): 0
> 0 大きい(後に来る)
42.CompareTo(15): 1

7. 複数ソート:フィールドを組み合わせる

ときどき、もっと複雑なソートが必要なこともある。たとえば、名字→名前→年齢の順で並べたい場合。さっきのやり方を応用して、順番に比較していけばOK:

public class Student : IComparable<Student>
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public int CompareTo(Student other)
    {
        if (other == null) return 1;
        int lastNameCompare = this.LastName.CompareTo(other.LastName);
        if (lastNameCompare != 0) return lastNameCompare;
        int firstNameCompare = this.FirstName.CompareTo(other.FirstName);
        if (firstNameCompare != 0) return firstNameCompare;
        return this.Grade.CompareTo(other.Grade);
    }
}

8. IComparable<T> を実装しない方がいい場合

プログラマーあるある:オブジェクトに「自然な」ソート順がないなら、IComparable<T> を実装しない方がいい。たとえば Point クラスで、XでソートするのかYなのか、原点からの距離なのか分からない場合は、外から比較関数(IComparer<T>)を渡す方がベター。この話は次のレクチャーで!

2
タスク
C# SELF, レベル 30, レッスン 0
ロック未解除
IComparableインターフェースを使って1つのフィールドで比較する
IComparableインターフェースを使って1つのフィールドで比較する
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION