1. 前言
想像一下,你是大學院長,學校裡有超多學生。你常常需要根據不同條件拿到排序好的名單:
- 依名字(要快速用字母找學生)。
- 依平均分數(要發獎學金給學霸)。
- 依年齡(做統計、比賽什麼的)。
- 依年級,然後年級內再依姓氏。
如果我們只靠 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 / 介面 | 在哪裡實作 | 用途 | 使用範例 |
|---|---|---|---|
|
直接在型別(class/struct)裡 | 一種標準化的比較方式 | 依 ID 升序排序 |
|
在獨立 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 裡還有很多地方會用到它,比如:
- 像 SortedList<TKey, TValue> 跟 SortedSet<T> 這種 collection 的建構子:你要指定元素的順序。
- 用 BinarySearch 搜尋時。
舉個例子:
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,等等。
視覺小抄:什麼時候用什麼?
| 需求 | 該用什麼 | 邏輯寫在哪 |
|---|---|---|
| 一種「自然」排序 | |
型別(class/struct)本身 |
| 多種排序方式 | |
獨立 comparator class |
| 臨時、一次性、很快搞定 | / lambda |
直接寫在 Sort 方法參數,用 delegate |
| 複雜、常用的比較邏輯 | |
獨立 comparator class |
下一堂課你會學到怎麼用 delegate 跟 lambda 來比較物件。現在先試著幫你的應用寫幾個不同的 comparator,享受一下這種排序邏輯完全獨立、超彈性的架構吧!
6. 實作 comparator 時常見錯誤
錯誤一:沒檢查 null。
如果比較的物件有一個是 null,你又沒處理,程式就會噴 NullReferenceException。
錯誤二:-1、0、+1 回傳值不對。
Compare 方法要回傳負數代表第一個小於第二個,零代表相等,正數代表大於。搞錯這個規則,排序就會「怪怪的」。
錯誤三:比較邏輯不對稱。
如果你比 x 跟 y 回傳一個值,然後比 y 跟 x 也回傳一樣(而不是相反),結果會很難預期。
錯誤四:自訂型別用 Sort() 沒給 comparator。
如果型別沒實作 IComparable 或 IComparable<T>,你又直接呼叫 Sort(),就會噴 InvalidOperationException。
怎麼避免:
記得檢查邊界狀況,重要的程式碼要寫單元測試(之後會教!),有問題就去翻文件——讓你的 comparator 跑得跟瑞士鐘錶一樣準!
GO TO FULL VERSION