1. 引言
想象一下,你让朋友把书摆到书架上。如果这些书脊上有编号(1、2、3……),你朋友肯定没问题:“OK,1在2前面,2在3前面。” 这就是自然顺序(按顺序)。同理,如果书名是“A”、“B”、“C”开头,他也会按字母表排。这也是字符串的“自然”顺序(按字母表)。
但如果书脊上只有作者姓氏呢?你说:“按顺序摆。” 你朋友会问:“按什么顺序?按姓?按出版年?按页数?” 这就麻烦了。对于你自定义的东西,没有明显的自然顺序。
编程里也是这样。当我们想排序一个整数列表 List<int>,C#知道怎么做。对于 List<string> 也没问题,用字典序(字母表顺序)。但如果我们有 List<Student>,每个 Student 是个对象,包含名字、姓氏、年龄、ID,还有一堆别的,C#就懵了。他不知道该按哪个属性比——名字?ID?平均分?这就是我们在“集合排序”那节课遇到的问题,当 List<T>.Sort() 试图排序自定义类型时直接报错。
要解决这个谜题,我们得给C#明确指示,按什么标准比较对象。为此就有了接口 IComparable<T>。
2. 接口 IComparable<T>
所以,为了让我们的对象“知道”自己和同类对象的先后关系,我们用接口 IComparable<T>。这就是个契约。当你的类或结构体实现了这个接口,你就像在跟编译器说:“嗨!我的对象可以互相比较,方法如下。”
怎么用?
接口 IComparable<T> 只定义了一个方法:
public interface IComparable<in T>
{
int CompareTo(T other);
}
这个方法接收一个 T 类型的对象(和当前对象同类型),返回:
- 负数(< 0),如果当前对象“小于”被比较的对象;
- 零(0),如果它们“相等”(排序角度看);
- 正数(> 0),如果当前对象“大于”。
本质上,这就像比赛的裁判,比如拳击:你表现更好就赢得回合,得分高;弱一点就低;打平就一样分。这里只是用带符号的数字,甚至更简单。
为啥要这样?
排序方法(比如 List<T>.Sort())会对列表元素调用 CompareTo,判断谁在前谁在后。如果你的类实现了这个接口——它就能被排序!
3. 实战
假设我们有这样一个用户类(User):
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
来试试排序用户列表:
List<User> users = new List<User>
{
new User { Name = "谢尔盖", Age = 31 },
new User { Name = "玛丽亚", Age = 22 },
new User { Name = "安东", Age = 27 }
};
users.Sort(); // 砰!InvalidOperationException
报错了:"At least one object must implement IComparable"(至少有一个对象要实现IComparable)。
修正:实现 IComparable<User>
给我们的类加上接口。先按年龄排序——从小到大:
public class User : IComparable<User>
{
public string Name { get; set; }
public int Age { get; set; }
public int CompareTo(User other)
{
// 防呆:如果other == null,当前用户“更大”
if (other == null) return 1;
return this.Age.CompareTo(other.Age); // 按年龄排序
}
}
现在建个列表,调用 users.Sort();,在控制台打印结果:
List<User> users = new List<User>
{
new User { Name = "谢尔盖", Age = 31 },
new User { Name = "玛丽亚", Age = 22 },
new User { Name = "安东", Age = 27 }
};
// 按年龄排序(用CompareTo)
users.Sort();
// 打印排序后的用户
foreach (User user in users)
{
Console.WriteLine($"{user.Name}, {user.Age}");
}
列表会按年龄排序:
玛丽亚, 22
安东, 27
谢尔盖, 31
可视化:排序前后
| 名字 | 年龄 |
|---|---|
| 谢尔盖 | 31 |
| 玛丽亚 | 22 |
| 安东 | 27 |
排序后:
| 名字 | 年龄 |
|---|---|
| 玛丽亚 | 22 |
| 安东 | 27 |
| 谢尔盖 | 31 |
4. 重要细节和常见错误
防 null
在 CompareTo 里一定要检查 other 不等于 null。不然可能会遇到 NullReferenceException —— 这种bug就像发布前的隐藏炸弹。通常,如果被比较对象 null,默认当前对象“更大”:
public int CompareTo(User other)
{
if (other == null) return 1;
// ...
}
保持传递性
如果A < B且B < C,那A也得< C。如果不遵守这个规则,排序会变得很迷(也许有趣,但肯定不对!)。
如果要按多个字段排序
比如,先按年龄,再按名字字母表。如果两个用户同龄,就按名字排。这样写:
public int CompareTo(User other)
{
if (other == null) return 1;
int ageCompare = this.Age.CompareTo(other.Age);
if (ageCompare != 0) return ageCompare;
// 年龄一样就按名字比
return this.Name.CompareTo(other.Name);
}
5. 排序:你的对象也能玩
List<int> 能做的,你的类也能做
现在可以用任何需要比较的方法了:Sort、BinarySearch,甚至插入到有序集合(比如 SortedSet<T>)。
users.Sort();
// users现在按年龄(同龄时按名字)排序了
课程应用里的例子
假设你之前已经写过用户管理应用。现在我们可以直接在已有代码里加上“自然”排序。效果如下:
// User类已经实现了IComparable<User>
List<User> users = new List<User>
{
new User { Name = "伊万", Age = 45 },
new User { Name = "加琳娜", Age = 27 },
new User { Name = "尤里", Age = 27 }
};
// 先按年龄,再按名字排序
users.Sort();
foreach (var u in users)
{
Console.WriteLine($"{u.Name} - {u.Age}");
}
结果:
加琳娜 - 27
尤里 - 27
伊万 - 45
加琳娜和尤里同龄,名字按字母表排。
6. CompareTo怎么写:硬核数学
再强调一下返回值标准:
- 负数(比如 -1):当前对象排在被比较对象前面。
- 零:排序上认为相等。
- 正数(比如 1):当前对象排在被比较对象后面。
内置类型(比如 Age.CompareTo(other.Age))已经实现了这个标准,总是返回 -1、0 或 1。
CompareTo 方法返回值表
| 返回值 | 啥意思? | 例子 |
|---|---|---|
| < 0 | 更小(排前面) | |
| 0 | 相等 | |
| > 0 | 更大(排后面) | |
7. 多重排序:字段组合
有时候要更复杂的排序,比如按姓、名、年级。用刚才的方法,依次比较就行:
public class Student : IComparable<Student>
{
public string LastName { get; set; }
public string FirstName { get; set; }
public int Grade { get; set; }
public int CompareTo(Student other)
{
if (other == null) return 1;
int lastNameCompare = this.LastName.CompareTo(other.LastName);
if (lastNameCompare != 0) return lastNameCompare;
int firstNameCompare = this.FirstName.CompareTo(other.FirstName);
if (firstNameCompare != 0) return firstNameCompare;
return this.Grade.CompareTo(other.Grade);
}
}
8. 什么时候别实现 IComparable<T>
现实(程序员的!)场景:如果一个对象没有“自然”排序顺序,最好别实现 IComparable<T>。比如你有个 Point 类,但你不知道该按X、Y还是到原点的距离排——那就让排序逻辑外部传进来,用比较器函数(IComparer<T>)。下节课我们会讲这个。
GO TO FULL VERSION