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. 生活中的例子:多種排序條件

假設我們有一個 User 類別:

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> 排序

方法一:經典寫法 — 寫一個獨立的比較器。

// 按姓和名排序的比較器
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})");
}

// 結果 — 使用者會按姓排序,如果姓一樣就比名字。

小提醒: 如果排序規則在多個地方會用到,而且 lambda 已經救不了你時,就這樣寫。

用 lambda「即時」排序

不想為了一次性的排序寫一堆 class?lambda 來救你!

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 } // 姓名一樣但年齡不同的重複
};

// "伊凡 彼得羅夫" 不管年齡,只會有一個進集合
Console.WriteLine("SortedSet 裡的使用者:");
foreach (var user in sortedUsersByFullName)
    Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");

SortedSet 不會加兩個「伊凡 彼得羅夫」!

重點: 這裡比較器決定「唯一性」的邏輯。如果你只比姓和名,年齡不同但同名同姓的使用者會被當成同一個。

5. 表格:.NET 裡什麼時候用哪種比較器

情境 要實作什麼 用法範例
自然排序(類型的唯一排序規則)
IComparable<T>
List<T>.Sort()
多種排序方式(按名字、年齡、e-mail ...)
IComparer<T>
(class、lambda)
List<T>.Sort(comparer)
在集合裡找唯一元素
IEqualityComparer<T>
HashSet<T>
Dictionary<TKey, TValue>
分組、去重
IEqualityComparer<T>
LINQ
.Distinct(comparer)
按時間、日期或複雜欄位組合排序
Comparison<T>
、即時 lambda
List<T>.Sort((a, b) => ...)
LINQ 裡用(一次性查詢)
OrderBy
ThenBy
裡寫 lambda
OrderBy(x => x.Name)

6. 實作練習:在集合裡查找和比較

來寫一個註冊功能,e-mail 唯一(不分大小寫)。如果已經有這個 e-mail,要提示用戶。

public static bool RegisterUser(List<User> users, User newUser)
{
    // 用 Any + lambda 查唯一 e-mail
    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