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

接口 IComparer<T>

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

1. 入门

想象一下,你是大学院长,手下有一大堆学生。你经常需要按不同标准排序名单:

  1. 按名字(方便你按字母表找人)。
  2. 按平均分(给学霸发奖学金)。
  3. 按年龄(做统计、比赛啥的)。
  4. 按年级,年级里再按姓氏。

如果我们只靠 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> 有啥区别

类 / 接口 实现位置 用途 使用例子
IComparable<T>
直接在类型(类/结构体)里 一种标准化的比较方式 按 ID 升序排序
IComparer<T>
在单独的类里 任意多种比较方式 按名字、日期排序

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 里还有这些地方会遇到它:

比如这样:


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,等等。

图解:啥时候用啥?

需求 用啥 逻辑写哪
一种“自然”排序
IComparable<T>
类型(类/结构体)里
多种排序方式
IComparer<T>
单独的比较器类
临时、一次性、随手写
Comparison<T>
/ lambda
Sort 方法参数里,写委托
复杂、经常用的逻辑
IComparer<T>
单独的比较器类

下节课你会学到怎么用和组合委托、lambda 表达式来比较对象。现在你可以试着给自己的应用写几个不同的比较器,感受下这种优雅的架构:排序逻辑和主类分离,排序标准选择灵活又可扩展。

6. 实现比较器时常见的坑

坑 1:没检查 null
如果比较的对象有一个是 null,你代码里没处理,程序就可能崩掉,报 NullReferenceException

坑 2:-10+1 返回值不对。
Compare 方法必须:第一个对象小于第二个返回负数,相等返回 0,大于返回正数。搞错了排序就会乱七八糟。

坑 3:比较逻辑不对称。
如果你比较 xy 返回一个值,比较 yx 也返回一样(而不是相反),结果就会很迷。

坑 4:自定义类型用 Sort() 没传比较器。
如果类型没实现 IComparableIComparable<T>,直接用 Sort() 不传比较器会抛 InvalidOperationException

怎么避免:
检查边界情况,关键代码多写单元测试(后面还会讲!),多看文档——让你的比较器像瑞士手表一样靠谱!

2
任务
C# SELF, 第 30 级, 课程 1
已锁定
实现字符串的比较器
实现字符串的比较器
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION