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
前两个是按字节比——不管文化和语言,所以ß和SS不一样。第三个用的是当前文化(比如德语),字符串就按母语者的习惯理解:ß当成ss,大小写也不管。所以eq3会返回true。
怎么选对的比较方式?
- Ordinal——快,按字节,适合技术场景(比如比ID)。
- CurrentCulture / InvariantCulture——用户文本,按操作系统或指定文化的规则。
在Sort、Compare等方法里,最好明确指定比较方式:
string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase)
不同语言下的排序特点
俄语排序——ё有时排在е后面,有时算一样(看文化!)。所以如果做严肃的东西(比如字典),最好问清楚客户或业务分析师,特殊字母到底怎么排。
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——它们是稳定的。
带可选/nullable字段的比较
.NET里经常有模型字段是DateTime?(Nullable<DateTime>)或int?。逻辑很简单:null比非null小,或者反过来——看需求。可以用标准库的辅助方法:
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安全",但老代码可不一定。一定要加保护,或者用合适的方法。
别用"魔法数字"
不要写return x.Field - y.Field;,尤其是可能溢出的字段(overflow)。如果是long类型——肯定有坑。
用StringComparison
别指望字符串默认比较。一定要明确传StringComparison.OrdinalIgnoreCase或其他合适的。
区分比较和相等
IComparable<T>和IEqualityComparer<T>是两回事。排序用比较器,查唯一用等价器。有时候结果还真不一样!
给"非典型"情况加测试
检查下排序在字段相等、字段为空、字符串大小写或语言不同的情况下是不是对的。
GO TO FULL VERSION