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

介面 IComparable<T>

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

1. 前言

想像一下,你請朋友幫你把書放到書架上。如果這些書的書脊上有編號(123…),你朋友一定沒問題:「OK,12 前面,23 前面。」這就是自然順序(照順序)。同樣,如果書名是「A」、「B」、「C」開頭,他也會照字母順序排。這也是字串的「自然」順序(字母排序)。

但如果書脊上只寫了作者的姓?你說:「幫我排一下順序。」你朋友會問:「哪種順序?按姓?按出版年?還是頁數?」這時候就有問題了。對於你自訂的東西,沒有明顯的自然順序。

寫程式也是一樣。我們要排序一個整數 list List<int>,C# 很清楚怎麼做。對 List<string> 也沒問題,會用字母順序。但如果你有 List<Student>,每個 Student 是一個物件,裡面有名字、姓氏、年齡、ID 還有一堆東西,C# 就傻眼了。他不知道要比哪個欄位。名字?ID?平均分數?這就是我們在「集合排序」那堂課遇到的問題,當你對 List<T>.Sort() 嘗試排序自訂型別時,會直接噴錯。

要解這個謎題,我們得給 C# 一個明確的指示,告訴他要怎麼比我們的物件。這時候就要用 IComparable<T> 這個介面啦。

2. 介面 IComparable<T>

所以,為了讓我們的物件「知道」自己跟其他同類的順序,我們會用 IComparable<T> 這個介面。這是一個合約。當你的 class 或 struct 實作這個介面時,你就像跟編譯器說:「嗨!我的物件可以互相比較,這裡有說明怎麼比。」

怎麼運作?

介面 IComparable<T> 只定義了一個方法:

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

這個方法會接收一個 T 型別的物件(跟自己同型別),然後要回傳:

  • 負數(< 0),如果目前這個物件「小於」被比較的那個;
  • 零(0),如果它們「相等」(以排序來看);
  • 正數(> 0),如果目前這個物件「大於」對方。

其實這就像比賽的裁判,比如拳擊賽:你表現比較好就贏這回合,分數比較高;表現差就分數低;如果打平就一樣分。這裡更簡單:只要一個有正負號的數字。

為什麼要這樣?

排序的方法(像 List<T>.Sort())會呼叫 CompareTo 來比 list 裡的元素,決定誰要排前面誰要排後面。如果你的 class 有實作這個介面——它就可以被排序啦!

3. 實作練習

假設我們有這樣一個使用者 class(User):

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

來試試看排序使用者 list:

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"(至少有一個物件要實作 IComparable)。

修正:實作 IComparable<User>

加上介面到我們的 class。先讓排序照年齡——從年輕到年長:

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); // 按年齡排序
    }
}

現在組一個 list,呼叫 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}");
}

list 會照年齡排序:

瑪麗亞, 22
安東, 27
謝爾蓋, 31

視覺化:排序前後

名字 年齡
謝爾蓋 31
瑪麗亞 22
安東 27

排序後:

名字 年齡
瑪麗亞 22
安東 27
謝爾蓋 31

4. 重要細節與常見錯誤

防範 null

CompareTo 裡面很重要的一點是要檢查 other 有沒有等於 null。如果沒注意這點,物件可能會噴 NullReferenceException——這種 bug 跟發佈前的 bug 一樣陰險。通常如果被比較的物件是 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. 排序:你的物件也可以!

List<int> 會的事,你的 class 也會了

現在你可以用任何需要比較的方法:SortBinarySearch,甚至插入到排序集合(像 SortedSet<T>)。

users.Sort();
// users 現在已經照年齡(同年齡再比名字)排序

課程應用程式裡的例子

假設你之前已經寫過一個管理使用者的 app。現在我們可以直接在現有程式碼裡加上「自然」排序。會長這樣:

// 我們的 User class 已經實作 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. 多重排序:欄位組合比較

有時候要更複雜的排序:比如先比姓氏,再比名字,最後比年級。用剛剛那個方法,可以依序比較:

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 class,但你不知道要比 X、Y 還是原點距離——那就讓排序邏輯從外面傳進來,用比較函式(IComparer<T>)。這個我們下堂課再聊。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION