CodeGym /課程 /C# SELF /介面 IComparer<T>

介面 IComparer<T>

C# SELF
等級 30 , 課堂 1
開放

1. 前言

想像一下,你是大學院長,學校裡有超多學生。你常常需要根據不同條件拿到排序好的名單:

  1. 依名字(要快速用字母找學生)。
  2. 依平均分數(要發獎學金給學霸)。
  3. 依年齡(做統計、比賽什麼的)。
  4. 依年級,然後年級內再依姓氏。

如果我們只靠 IComparable<T>,那 學生 這個 class 只能有 一種 比較方式。比如說我們決定「自然」順序就是依平均分數。這樣 List.Sort() 就能用了!但如果我們想要依名字排序呢?學生 這個 class 已經「被綁定」只能比分數,不能同時有兩種「自然」順序。這就像你只有一份「怎麼當拳擊冠軍」的說明書,突然要你去下西洋棋,你還硬要照拳擊的規則來玩,肯定不會贏啦,對吧?

這種情況下,如果你想對同一種物件用不同方式排序,又不想把比較邏輯塞進 class 裡,就要用 IComparer<T> 這個介面。這超有用,尤其是你沒辦法動 class 原始碼,或是 class 根本不該知道所有排序方式的時候。

2. 介面 IComparer<T>

如果說 IComparable<T> 是物件自己的直覺,知道怎麼跟別人比,那 IComparer<T> 就是 完全獨立的外部裁判獨立仲裁者,他拿兩個玩家(物件)來,照自己的規則比較誰贏。

想像你是足球隊教練,要選隊長。

  • IComparable<T>:每個球員自己說:「我比他強,因為我跑比較快!」(自己的內建規則)。
  • IComparer<T>:你這個教練說:「好啦,今天我們用傳球準度來選隊長。小明,傳球!小華,傳球!OK,小明比較準,他今天當隊長。」(這是你 外部 的規則,套用在任何兩個球員身上)。

定義: IComparer<T> 是 .NET 裡的介面,讓你可以定義 外部比較邏輯,用來比兩個 T 型別的物件。也就是說,實作 IComparer<T> 的 class 本身不是被比較的物件之一;它只是提供一個 Compare 方法,接收兩個物件,決定它們的相對順序。

語法

介面 IComparer<T> 的語法超簡單:


public interface IComparer<T>
{
    // 比較兩個 T 型別的物件
    // x - 第一個要比的物件
    // y - 第二個要比的物件
    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> 有什麼不一樣

Class / 介面 在哪裡實作 用途 使用範例
IComparable<T>
直接在型別(class/struct)裡 一種標準化的比較方式 依 ID 升序排序
IComparer<T>
在獨立 class 裡 可以有無限多種比較方式 依名字、日期排序

3. IComparer<T> 的實作範例

我們繼續來玩「一個大應用」——簡單的使用者模型。假設我們有這個 class:


// 我們的使用者 class
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

依名字排序:寫一個獨立的 comparator

來寫一個實作 IComparer<User> 的 class,專門拿來比名字:


// 依名字排序的 comparator class
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);
    }
}

用 comparator 來排序 list:


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)); // 安娜, 鮑里斯, 伊萬, 魯斯蘭

有沒有發現超優雅?comparator class 就像你程式裡的自由公民,想拿來比哪個 user list 都可以。

4. 多種比較方式:寫不同的 comparator

有趣的是,你想寫幾個 comparator 都沒問題。比如我們來寫一個依年齡排序的:


// 依年齡排序的 comparator
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)

如果你想要依年齡降序,只要把參數順序換一下就好:


// 依年齡降序的 comparator
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. 實用小細節

內部怎麼運作?

當你用 comparator 呼叫 Sort(),list 會把每個元素丟給 comparator 問:「誰要排前面?」你的 Compare 會回答:「這個、那個,或是不用動。」排序就會一直這樣問到排好為止。

如果值一樣怎麼辦?只要回傳 0,意思是順序不變(或交給排序內部機制決定)。

IComparer<T> 還會用在哪?

IComparer<T> 不只在 list 裡用得到,.NET 裡還有很多地方會用到它,比如:

舉個例子:


var sortedSet = new SortedSet<User>(new UserAgeComparer());

這樣 SortedSet 會自動一直維持依年齡排序!

null 安全小提醒

新手最常踩的雷之一就是 NullReferenceException。記得在 Compare 裡檢查 null,尤其是 list 裡可能有 null 的時候。

常見寫法(再貼一次,讓你記住):


if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;

這個習慣超好,可以避免你程式在最尷尬的時候掛掉!

IComparer<T> 的優點跟限制

  • 把比較邏輯跟資料分開。User class 不用管自己怎麼被排序。
  • 比較邏輯可以重複用在不同地方。
  • 超好擴充:你想加幾種排序都不用動原本的型別。

但要小心,不要「忘記檢查 null」或「比較邏輯不一致」。比如 Compare(x, y) 回傳 0,那 Compare(y, x) 也要回傳 0;Compare(x, y) 回傳 >0,那 Compare(y, x) 就要回傳 <0,等等。

視覺小抄:什麼時候用什麼?

需求 該用什麼 邏輯寫在哪
一種「自然」排序
IComparable<T>
型別(class/struct)本身
多種排序方式
IComparer<T>
獨立 comparator class
臨時、一次性、很快搞定
Comparison<T>
/ lambda
直接寫在 Sort 方法參數,用 delegate
複雜、常用的比較邏輯
IComparer<T>
獨立 comparator class

下一堂課你會學到怎麼用 delegate 跟 lambda 來比較物件。現在先試著幫你的應用寫幾個不同的 comparator,享受一下這種排序邏輯完全獨立、超彈性的架構吧!

6. 實作 comparator 時常見錯誤

錯誤一:沒檢查 null
如果比較的物件有一個是 null,你又沒處理,程式就會噴 NullReferenceException

錯誤二:-10+1 回傳值不對。
Compare 方法要回傳負數代表第一個小於第二個,零代表相等,正數代表大於。搞錯這個規則,排序就會「怪怪的」。

錯誤三:比較邏輯不對稱。
如果你比 xy 回傳一個值,然後比 yx 也回傳一樣(而不是相反),結果會很難預期。

錯誤四:自訂型別用 Sort() 沒給 comparator。
如果型別沒實作 IComparableIComparable<T>,你又直接呼叫 Sort(),就會噴 InvalidOperationException

怎麼避免:
記得檢查邊界狀況,重要的程式碼要寫單元測試(之後會教!),有問題就去翻文件——讓你的 comparator 跑得跟瑞士鐘錶一樣準!

2
任務
C# SELF, 等級 30, 課堂 1
上鎖
實作字串的比較器
實作字串的比較器
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION