CodeGym /课程 /C# SELF /.NET中比较器的实际应用

.NET中比较器的实际应用

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

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里各种比较器啥时候用

场景 要实现啥 用法例子
自然排序(类型通用的排序规则)
IComparable<T>
List<T>.Sort()
多种排序方式(按名、年龄、e-mail...)
IComparer<T>
(类、lambda)
List<T>.Sort(comparer)
集合里找唯一元素
IEqualityComparer<T>
HashSet<T>
、
Dictionary<TKey, TValue>
分组、去重
IEqualityComparer<T>
LINQ
.Distinct(comparer)
按时间、日期或复杂字段组合排序
Comparison<T>
、临时lambda
List<T>.Sort((a, b) => ...)
LINQ里用(一次性查询) 在
OrderBy
、
ThenBy
里写lambda
OrderBy(x => x.Name)

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 里就会乱套:明明有元素却找不到。

还有人只比对象的一个属性(比如只比姓),忘了可能有很多同姓的用户。结果对象被“覆盖”或丢失,数据就不一致了。也就是说,数据不再反映系统真实状态——会有重复、丢信息或者程序逻辑出错。

2
任务
C# SELF, 第 30 级, 课程 4
已锁定
创建一个用于按年龄排序的比较器
创建一个用于按年龄排序的比较器
1
调查/小测验
比较器第 30 级,课程 4
不可用
比较器
比较器和对象比较
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION