CodeGym /課程 /C# SELF /複雜比較情境與最佳實踐

複雜比較情境與最佳實踐

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

1. 多層級(階層式)排序

為什麼比較不總是那麼簡單?

C# 面試很愛問複雜物件的排序和比較問題。這不是流行,而是反映了每個開發者都會遇到的真實問題。像是資料庫查詢、介面上的表格排序、集合裡的唯一性——這些都直接跟物件比較的正確性有關。

如果只是比數字,超簡單。但如果我們想要先依姓氏、再依名字排序,還要考慮有些欄位可能是空的(null),或是字串用不同語言寫……這時候就需要有系統的方法了。

常見需求:先依一個欄位排序,如果一樣再比第二個,如果還一樣再比第三個。

範例:有名字、姓氏和生日的使用者


public class User
{
    public string FirstName { get; set; }
    public string LastName  { get; set; }
    public DateTime BirthDate { get; set; }

    // 為了美觀——顯示使用者資訊
    public override string ToString()
        => $"{LastName} {FirstName} ({BirthDate:yyyy-MM-dd})";
}

為什麼需要階層?

想像一下,我們有一個使用者清單,要照字母順序顯示:先依姓氏,再依名字。如果姓氏和名字都一樣——就比生日。

比較邏輯:"責任鏈"

這很像奧林匹亞比賽的排名:先比分數,分數一樣比交卷時間,再一樣就比字母順序。程式碼這樣寫很簡單:


public class UserComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // 比姓氏
        int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);

        if (result != 0) return result; // 不一樣就直接回傳

        // 姓氏一樣——比名字
        result = string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
        if (result != 0) return result;

        // 姓名都一樣——比生日
        return x.BirthDate.CompareTo(y.BirthDate);
    }
}

怎麼用:


var users = new List<User>
{
    new User { FirstName = "伊萬", LastName = "伊萬諾夫", BirthDate = new DateTime(1990, 1, 1) },
    new User { FirstName = "彼得", LastName = "伊萬諾夫", BirthDate = new DateTime(1992, 5, 1) },
    new User { FirstName = "安娜", LastName = "彼得羅娃", BirthDate = new DateTime(1985, 8, 30) }
};

users.Sort(new UserComparer());
users.ForEach(Console.WriteLine);
// 彼得羅娃 安娜 (1985-08-30)
// 伊萬諾夫 伊萬 (1990-01-01)
// 伊萬諾夫 彼得 (1992-05-01)

2. 字串怎麼比?文化差異

字串比較:Ordinal、CurrentCulture、InvariantCulture

字串看起來到哪都一樣,但其實沒那麼單純!像俄文的 ёе,德文的 ssß,還有大小寫...

.NET 裡字串比較有特別的規則,可以用 StringComparison 設定。這會影響排序和搜尋。

字串比較範例:


// 在德文裡 ß 幾乎等於 ss
string a = "straße";
string b = "STRASSE";

bool eq1 = string.Equals(a, b, StringComparison.Ordinal); // false
bool eq2 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // false
bool eq3 = string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase); // true 

前兩個 case 是用位元組比——不考慮文化和語言細節,所以 ßSS 被當成不同。但第三個 case 用的是目前文化(比如德文),字串會像母語者那樣解讀:ß 被當成 ss,大小寫也忽略。所以 eq3 會回傳 true

怎麼選對的比較方式?

  • Ordinal — 快速、位元組級,適合技術用途(像是比 id)。
  • CurrentCulture / InvariantCulture — 給使用者看的文字,會考慮作業系統或指定文化的規則。

SortCompare 等方法裡,記得明確指定比較方式:


string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase)

不同語言的排序特色

俄文排序——ё 可能排在 е 後面,也可能被當成一樣(看文化設定!)。所以如果你做很嚴謹的東西(比如字典),記得問清楚客戶或 BA,特殊字母要怎麼排。

3. null 防護:不是每個物件都乖乖的

欄位是 null 怎麼辦?

實際寫程式時,總有人會忘記填欄位,這時候一比就——砰!——NullReferenceException。我們要有心理準備。

沒防 null 的例子:


public int Compare(User x, User y)
{
    return x.LastName.CompareTo(y.LastName); // 如果 LastName == null,會出錯!
}

有防護的例子(null 比任何非 null 小):


public int Compare(User x, User y)
{
    // 用專門的字串比較器,會考慮 null 值
    int byLastName = Comparer<string>.Default.Compare(x.LastName, y.LastName);
    if (byLastName != 0) return byLastName;

    // 依此類推...
}

更短的寫法:


public int Compare(User x, User y)
{
    return string.Compare(x?.LastName, y?.LastName, StringComparison.OrdinalIgnoreCase);
}

"Null" 要排前還是排後?

你可以讓所有 "空" 姓氏的使用者排在清單最前或最後——看需求決定。


public int Compare(User x, User y)
{
    if (x.LastName == null && y.LastName == null) return 0;
    if (x.LastName == null) return 1;   // null 排最後
    if (y.LastName == null) return -1;  // null 排最後
    return string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
}

4. 多欄位比較

新手常犯的錯誤——用算術運算代替 "責任鏈" 比較,比如:


// 千萬別這樣寫!
public int Compare(User x, User y)
{
    // 壞範例
    return (x.Age - y.Age) + string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal);
}

這種寫法不能保證排序正確:如果年齡差 -100,字串比較回傳 1,結果就是 -99,這跟預期邏輯不符。

正確做法是明確的順序:
有差異就回傳,否則看下一個欄位。

5. 有用的小細節

如果物件主要欄位都一樣怎麼辦?

如果物件真的 "一樣",很重要的一點是排序演算法要 穩定:不會改變那些比較結果為相等的元素順序。內建的 List<T>.Sort() 不保證穩定。如果很在意(比如多層表格排序),用 LINQ 的 OrderBy/ThenBy——這些是穩定的。

考慮 optional/nullable 欄位的比較

.NET 常有像 DateTime?Nullable<DateTime>)或 int? 這種欄位。邏輯很簡單:null 比非 null 小,或反過來——看需求。可以用標準函式庫的 helper:


int result = Nullable.Compare<DateTime>(u1.BirthDate, u2.BirthDate);

有額外規則的比較

有時候要讓比較考慮某個欄位的 "權重",比如 VIP 客戶永遠排最前。做法——把 "VIP 標記" 放在排序最前面。


public int Compare(User x, User y)
{
    // VIP 排最前
    int vipResult = y.IsVip.CompareTo(x.IsVip); // true = 1, false = 0;降冪排序
    if (vipResult != 0) return vipResult;

    // 其他照慣例
    int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
    if (result != 0) return result;
    return string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
}

6. 建議與最佳實踐

永遠檢查 null
現代 C# 越來越強調 "null 安全",但舊程式碼還是會有 null。一定要加防護或用對的方法。

避免 "魔法數字"
不要寫 return x.Field - y.Field; 這種,尤其是可能溢位(overflow)的欄位。如果是 long,錯誤機率更高。

用 StringComparison
不要相信字串預設比較。明確傳 StringComparison.OrdinalIgnoreCase 或其他適合的選項。

分開比較和相等判斷
IComparable<T>IEqualityComparer<T> 是不同的事。排序用比較器,找唯一物件用等價判斷。有時候結果會不一樣!

加測試,特別是 "非典型" 情境
測試排序在欄位相等、欄位為空、字串大小寫或語言不同時是否正確。

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