1. 入门
想象一下,你是大学院长,手下有一大堆学生。你经常需要按不同标准排序名单:
- 按名字(方便你按字母表找人)。
- 按平均分(给学霸发奖学金)。
- 按年龄(做统计、比赛啥的)。
- 按年级,年级里再按姓氏。
如果我们只靠 IComparable<T>,那 学生 这个类只能实现 一种 比较方式。比如我们觉得“自然顺序”就是按平均分,那 List.Sort() 就能用了!但如果你想按名字排呢?学生 这个类已经“被占用”了,只能按分数比。它不能同时有两个“自然顺序”。这就像你只有一份“怎么成为拳击冠军”的说明书,但突然要去下国际象棋,还想用拳击的说明书,估计你也玩不转,对吧?
所以啊,遇到需要对同一类对象用不同方式排序、又不想把比较逻辑塞进类里的时候,就得用 IComparer<T> 接口。它特别适合你没法改类源码,或者类本身不该知道所有排序方式的场景。
2. IComparer<T> 接口
如果说 IComparable<T> 是对象的“内心直觉”,知道怎么和别人比,那 IComparer<T> 就是 完全独立的外部裁判 或 独立仲裁者,它拿两个玩家(对象)来,按自己定的规则比较。
想象你是足球队教练,要选队长。
- IComparable<T>:每个球员自己说:“我比他强,因为我跑得快!”(自己的内心规则)。
- IComparer<T>:你作为教练说:“兄弟们,今天我们按传球准度选队长。小明,传一个!小王,传一个!好,小明更准!今天他当队长。”(这是你 外部 的规则,适用于任何两个球员)。
定义: IComparer<T> 是 .NET 里的一个接口,让你能为 T 类型的两个对象定义 外部比较逻辑。实现 IComparer<T> 的类本身不是被比较的对象;它只是提供一个 Compare 方法,接收两个对象,决定它们的相对顺序。
语法
IComparer<T> 接口的语法很简单:
public interface IComparer<T>
{
// 比较两个 T 类型对象的方法
// x - 第一个要比较的对象
// y - 第二个要比较的对象
int Compare(T x, T y);
}
Compare(T x, T y) 方法和 CompareTo(T other) 在 IComparable<T> 里用法一样:
- 如果 x “小于” y,返回 负数。
- 如果 x “等于” y,返回 零 (0)。
- 如果 x “大于” y,返回 正数。
这里的“小于”、“等于”、“大于”完全取决于你在 Compare 里怎么写比较逻辑。
IComparer<T> 和 IComparable<T> 有啥区别
| 类 / 接口 | 实现位置 | 用途 | 使用例子 |
|---|---|---|---|
|
直接在类型(类/结构体)里 | 一种标准化的比较方式 | 按 ID 升序排序 |
|
在单独的类里 | 任意多种比较方式 | 按名字、日期排序 |
3. IComparer<T> 实战
我们继续完善我们的“小型应用”——一个简单的用户模型。假设有这样一个类:
// 我们的用户类
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
按名字排序:写个专门的比较器
写个实现 IComparer<User> 的类,专门按名字比:
// 按名字排序的比较器类
public class UserNameComparer : IComparer<User>
{
public int Compare(User x, User y)
{
// 检查 null(防止踩坑!)
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1; // null 比任何对象“小”
if (y is null) return 1;
// 按名字比(用字符串标准排序)
return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
}
}
用比较器排序列表:
List<User> users = new List<User>
{
new User { Name = "伊万", Age = 20, Email = "ivan@mail.com" },
new User { Name = "安娜", Age = 32, Email = "anna@gmail.com" },
new User { Name = "鲍里斯", Age = 28, Email = "boris@work.org" },
new User { Name = "鲁斯兰", Age = 19, Email = "ruslan@yandex.ru" }
};
// 用 IComparer 按名字排序
users.Sort(new UserNameComparer());
users.ForEach(u => Console.WriteLine(u.Name)); // 安娜, 鲍里斯, 伊万, 鲁斯兰
看到没,这样写又优雅又清晰!比较器类就像你程序里的独立公民,随便哪个用户列表都能用它。
4. 多种比较方式:写不同的比较器
有意思的是,你想写多少比较器都行。比如我们来实现按年龄排序:
// 按年龄排序的比较器
public class UserAgeComparer : 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;
// 按年龄升序
return x.Age.CompareTo(y.Age);
}
}
现在:
users.Sort(new UserAgeComparer());
users.ForEach(u => Console.WriteLine($"{u.Name} ({u.Age})"));
// 输出: 鲁斯兰 (19) 伊万 (20) 鲍里斯 (28) 安娜 (32)
如果你想按年龄降序,只要把参数换下顺序就行:
// 按年龄降序的比较器
public class UserAgeDescendingComparer : 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;
// 换顺序: y.CompareTo(x)
return y.Age.CompareTo(x.Age);
}
}
5. 实用细节
底层是怎么工作的?
当你用比较器调用 Sort(),列表会把每个元素交给比较器问:“谁应该排前面?”你的 Compare 回答:“这个,这个,或者随便。”排序会对所有组合重复这个对话,直到排好序。
如果值相等怎么办?直接返回 0,意思是顺序不变(或者由排序内部机制决定)。
IComparer<T> 还能用在哪?
IComparer<T> 不只在列表里用,.NET 里还有这些地方会遇到它:
- 比如 SortedList<TKey, TValue> 和 SortedSet<T> 这些集合的构造函数:你得指定元素顺序。
- 用 BinarySearch 查找时。
比如这样:
var sortedSet = new SortedSet<User>(new UserAgeComparer());
现在 SortedSet 会自动按年龄排序!
null 安全小贴士
新手最常见的坑之一就是 NullReferenceException。记得在 Compare 里检查 null,特别是列表可能有 null 的时候。
常用模式(再贴一次,记住!):
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
养成这个习惯,关键时刻能救命!
用 IComparer<T> 的优缺点
- 把比较逻辑和数据分开。用户类不用管怎么、为什么被排序。
- 比较逻辑能在不同地方复用。
- 可扩展性强:你想加多少种排序都不用改原始类型。
但也要小心别踩“忘了 null”或“比较逻辑不一致”的坑。比如 Compare(x, y) 返回 0,那 Compare(y, x) 也得返回 0;Compare(x, y) 返回 >0,那 Compare(y, x) 必须返回 <0,等等。
图解:啥时候用啥?
| 需求 | 用啥 | 逻辑写哪 |
|---|---|---|
| 一种“自然”排序 | |
类型(类/结构体)里 |
| 多种排序方式 | |
单独的比较器类 |
| 临时、一次性、随手写 | / lambda |
Sort 方法参数里,写委托 |
| 复杂、经常用的逻辑 | |
单独的比较器类 |
下节课你会学到怎么用和组合委托、lambda 表达式来比较对象。现在你可以试着给自己的应用写几个不同的比较器,感受下这种优雅的架构:排序逻辑和主类分离,排序标准选择灵活又可扩展。
6. 实现比较器时常见的坑
坑 1:没检查 null。
如果比较的对象有一个是 null,你代码里没处理,程序就可能崩掉,报 NullReferenceException。
坑 2:-1、0、+1 返回值不对。
Compare 方法必须:第一个对象小于第二个返回负数,相等返回 0,大于返回正数。搞错了排序就会乱七八糟。
坑 3:比较逻辑不对称。
如果你比较 x 和 y 返回一个值,比较 y 和 x 也返回一样(而不是相反),结果就会很迷。
坑 4:自定义类型用 Sort() 没传比较器。
如果类型没实现 IComparable 或 IComparable<T>,直接用 Sort() 不传比较器会抛 InvalidOperationException。
怎么避免:
检查边界情况,关键代码多写单元测试(后面还会讲!),多看文档——让你的比较器像瑞士手表一样靠谱!
GO TO FULL VERSION