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 — 給使用者看的文字,會考慮作業系統或指定文化的規則。
在 Sort、Compare 等方法裡,記得明確指定比較方式:
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> 是不同的事。排序用比較器,找唯一物件用等價判斷。有時候結果會不一樣!
加測試,特別是 "非典型" 情境
測試排序在欄位相等、欄位為空、字串大小寫或語言不同時是否正確。
GO TO FULL VERSION