CodeGym /课程 /C# SELF /接口 IComparable<T>

接口 IComparable<T>

C# SELF
第 30 级 , 课程 0
可用

1. 引言

想象一下,你让朋友把书摆到书架上。如果这些书脊上有编号(123……),你朋友肯定没问题:“OK,12前面,23前面。” 这就是自然顺序(按顺序)。同理,如果书名是“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> 能做的,你的类也能做

现在可以用任何需要比较的方法了:SortBinarySearch,甚至插入到有序集合(比如 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))已经实现了这个标准,总是返回 -101

CompareTo 方法返回值表

返回值 啥意思? 例子
< 0 更小(排前面)
20.CompareTo(21): -1
0 相等
23.CompareTo(23): 0
> 0 更大(排后面)
42.CompareTo(15): 1

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>)。下节课我们会讲这个。

2
任务
C# SELF, 第 30 级, 课程 0
已锁定
为单个字段实现 IComparable 接口进行比较
为单个字段实现 IComparable 接口进行比较
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION