1. 入门
为啥比较器真的很重要…
在真实项目里,事情可没那么简单:对象很复杂,数据可能是用户输的,也可能是别的服务给的,我们想要的“排序规则”(甚至“相等规则”)经常会根据需求变。比较器能让你的代码更灵活,行为也更可控、更容易预测。
在.NET里,每当你要对自定义类对象排序、查找、分组或者去重,或者你要搞有“顺序”的数据结构时,比较器就必不可少。尤其是在集合、算法和跟外部API集成时,这种需求特别多。
比较器在.NET里都用在哪:
- 集合排序(List<T>.Sort、Array.Sort、OrderBy 在LINQ里)
- 有序数据结构(SortedSet<T>、SortedDictionary<TKey, TValue>)
- 在集合里查找和比较对象(Contains、IndexOf —— 这时候要判断“相等”,不只是顺序)
- 分组、过滤和去重(比如 .Distinct())
实际开发中为啥要用?
- 报表输出要排序(比如学生名单按字母顺序或者平均分)
- 把输入数据和标准值对比(比如按某个条件查找记录)
- 省内存和提速(选对数据结构能让查找更快,程序更流畅)
- 校验唯一性(比如注册用户时用e-mail查重)
2. 生活中的例子:按不同规则排序
假设我们有个用户类:
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> 排序
方法1:老派做法——写个单独的比较器类。
// 按姓和名排序的比较器
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“临时”排序
不想专门写类?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 时,Equals 和 GetHashCode 都要实现。忘了第二个,集合查找就会出幺蛾子。
4. SortedSet 和 SortedDictionary 的用法
这时候比较器的威力就体现出来了。
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里各种比较器啥时候用
| 场景 | 要实现啥 | 用法例子 |
|---|---|---|
| 自然排序(类型通用的排序规则) | |
|
| 多种排序方式(按名、年龄、e-mail...) | (类、lambda) |
|
| 集合里找唯一元素 | |
、 |
| 分组、去重 | |
LINQ |
| 按时间、日期或复杂字段组合排序 | 、临时lambda |
|
| LINQ里用(一次性查询) | 在 、 里写lambda |
|
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 说对象不一样,比较器却说一样,这样 SortedSet 或 SortedDictionary 里就会乱套:明明有元素却找不到。
还有人只比对象的一个属性(比如只比姓),忘了可能有很多同姓的用户。结果对象被“覆盖”或丢失,数据就不一致了。也就是说,数据不再反映系统真实状态——会有重复、丢信息或者程序逻辑出错。
GO TO FULL VERSION