在 C# 开发中,对集合进行高效的关系运算,比如去重、合并、查找共同项或差异项,是数据处理的基本功。LINQ 提供了一组强大的集合关系操作方法,其中 Distinct、Union、Intersect 和 Except 是最核心的四个。本文将深入剖析它们的用法、原理,并结合 .NET 6 引入的新方法,通过一个模拟 MVVM 数据处理场景的实战练习,帮助你彻底掌握。
一、Distinct(去重)
功能
- 移除序列中的重复元素,返回一个由唯一元素构成的新集合。
- 默认使用元素类型的
Equals 和 GetHashCode 方法来判定唯一性。
- 可通过传递一个
IEqualityComparer<T> 比较器来自定义去重规则。
延迟执行
- 与其他大多数
LINQ 算子一样,Distinct 采用延迟执行策略,即不枚举则不执行。
- 只有在实际遍历结果序列(例如使用
foreach)时,去重操作才会发生。
基础示例
var names = new List<string> { “Alice”, “Bob”, “Alice”, “Charlie” };
var uniqueNames = names.Distinct();
foreach (var name in uniqueNames)
{
Console.WriteLine(name);
}
// 输出顺序:Alice, Bob, Charlie
思考与实践:如何自定义去重规则?
-
Distinct 对引用类型的默认判定依据是什么?
Distinct() 默认使用对象的 GetHashCode() 和 Equals() 进行比较。这意味着对于自定义类,你需要正确重写这两个方法,否则会根据引用地址判断,可能达不到预期效果。
-
如果希望按自定义条件(如 Student.Name)去重,怎么做?
方法一:使用内置的 StringComparer(最推荐:处理字符串时)
如果你只是想处理字符串的大小写问题,不需要自己写类,直接传入 .NET 自带的比较器即可。
var names = new List<string> { “Alice”, “alice”, “Bob” };
// 忽略大小写去重
var uniqueNames = names.Distinct(StringComparer.OrdinalIgnoreCase);
foreach (var name in uniqueNames)
{
Console.WriteLine(name); // 输出:Alice, Bob
}
方法二:实现 IEqualityComparer<T> 接口(最标准:适用于复杂逻辑)
如果你有更特殊的规则(比如:只要首字母相同就视为同一个名字),你需要创建一个自定义的比较器类。理解并实现这个接口,是处理复杂对象比较的关键,也是很多算法和数据处理逻辑的基础。
public class FirstLetterComparer : IEqualityComparer<string>
{
// 判断两个对象是否相等
public bool Equals(string x, string y)
{
if (x == null || y == null) return false;
return x[0] == y[0]; // 规则:首字母相同即相等
}
// 计算哈希值
// 注意:Equals 返回 true 的两个对象,GetHashCode 必须也返回相同的值
public int GetHashCode(string obj)
{
return obj == null ? 0 : obj[0].GetHashCode();
}
}
// 使用方式:
var uniqueNames = names.Distinct(new FirstLetterComparer());
方法三:使用 LINQ 的 DistinctBy(.NET 6+ 最便捷)
如果在处理对象列表(例如 List<User>),且只想根据对象的某个字段去重,DistinctBy 是性能最高且最简洁的选择。
var users = new List<User>
{
new User { Id = 1, Name = “Alice” },
new User { Id = 2, Name = “Alice” }, // Name 重复
new User { Id = 3, Name = “Bob” }
};
// 根据 Name 属性去重,保留遇到的第一个对象
var uniqueUsers = users.DistinctBy(u => u.Name);
foreach (var user in uniqueUsers)
{
Console.WriteLine(user.Id + “: “ + user.Name);
}
// 输出:
// 1: Alice
// 3: Bob
二、Union(并集)
功能
- 合并两个序列,并自动去除重复项。
- 返回的是两个序列中所有唯一元素组成的新序列。
延迟执行
- 同样是延迟执行,遍历时才会计算并集。
- 可使用默认比较规则或通过
IEqualityComparer<T> 指定自定义比较器。
基础示例
var listA = new List<string> { “Alice”, “Bob” };
var listB = new List<string> { “Bob”, “Charlie” };
var union = listA.Union(listB);
foreach (var name in union)
{
Console.WriteLine(name);
}
// 输出顺序:Alice, Bob, Charlie
思考与实践:如何自定义合并规则?
-
Union 是否改变原集合?
不改变,它返回的是一个全新的序列。
-
如何让 Union 使用自定义比较规则?
1. 简单字符串:忽略大小写合并
这是最常见的需求。如果你希望 “Alice” 和 “alice” 被视为同一个元素,只需传入 StringComparer。
var listA = new List<string> { “Alice”, “Bob” };
var listB = new List<string> { “alice”, “Charlie” };
// 使用内置比较器
var union = listA.Union(listB, StringComparer.OrdinalIgnoreCase);
foreach (var name in union)
{
Console.WriteLine(name);
}
// 输出:Alice, Bob, Charlie (注意:保留的是第一次遇到的 “Alice”)
2. 复杂对象:根据特定属性合并 (UnionBy)
在 .NET 6及更高版本 中,如果你有两个对象列表(比如从两个不同的数据库查出来的用户信息),想根据 Id 或 Name 合并,UnionBy 是最高效的选择。
var listA = new List<User> { new User(1, “Alice”), new User(2, “Bob”) };
var listB = new List<User> { new User(2, “Bob”), new User(3, “Charlie”) };
// 根据 User 的 Id 属性进行合并
var union = listA.UnionBy(listB, u => u.Id);
// 结果将包含 Id 为 1, 2, 3 的三个 User 对象
3. 高级自定义:实现 IEqualityComparer<T>
如果你需要更复杂的合并逻辑(例如:名字长度相同且首字母相同才算重复),你需要像 Distinct 那样实现一个自定义比较器。
public class MyCustomComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
// 自定义逻辑:长度相同即视为重复
return x?.Length == y?.Length;
}
public int GetHashCode(string obj)
{
return obj?.Length.GetHashCode() ?? 0;
}
}
// 使用
var union = listA.Union(listB, new MyCustomComparer());
三、Intersect(交集)
功能
- 返回两个序列中都存在的元素(即共同项)。
- 默认使用
Equals/GetHashCode 判定元素是否相同。
示例
var listA = new List<string> { “Alice”, “Bob” };
var listB = new List<string> { “Bob”, “Charlie” };
var intersection = listA.Intersect(listB);
foreach (var name in intersection)
{
Console.WriteLine(name);
}
// 输出:Bob
四、Except(差集)
功能
示例
var listA = new List<string> { “Alice”, “Bob”, “Charlie” };
var listB = new List<string> { “Bob” };
var except = listA.Except(listB);
foreach (var name in except)
{
Console.WriteLine(name);
}
// 输出:Alice, Charlie
五、实战编码练习(模拟 MVVM 数据处理)
场景:班级选课数据处理。我们需要对两个班级的选课学生名单进行各种关系运算。
首先,定义数据模型:
class Student
{
public string Name { get; set; }
public string Course { get; set; }
}
初始化数据:
var studentsA = new List<Student>
{
new Student { Name=“Alice”, Course=“Math” },
new Student { Name=“Bob”, Course=“English” },
new Student { Name=“Alice”, Course=“Math” } // 重复项
};
var studentsB = new List<Student>
{
new Student { Name=“Charlie”, Course=“Math” },
new Student { Name=“Bob”, Course=“English” }
};
现在,开始进行一系列集合操作:
// 1. 班级A学生去重(按 Name + Course 组合键)
var uniqueA = studentsA.DistinctBy(s => (s.Name, s.Course));
// 2. A 和 B 的学生并集(按 Name + Course 去重)
// 使用 EqualityComparer.Create 快速创建临时比较器,无需单独实现类
var unionAB = uniqueA.Union(studentsB, EqualityComparer<Student>.Create((x, y) => x.Name == y.Name && x.Course == y.Course, s => HashCode.Combine(s.Name, s.Course)));
// 3. 交集:A 和 B 都选课的学生(同名同课程)
var intersectAB = uniqueA.Intersect(studentsB, EqualityComparer<Student>.Create((x, y) => x.Name == y.Name && x.Course == y.Course, s => HashCode.Combine(s.Name, s.Course)));
// 4. 差集:只在A班选课,B班没有的学生
var exceptAB = uniqueA.Except(studentsB, EqualityComparer<Student>.Create((x, y) => x.Name == y.Name && x.Course == y.Course, s => HashCode.Combine(s.Name, s.Course)));
技术要点:这个练习综合运用了 .NET 6+ 的 DistinctBy 和通过 EqualityComparer.Create 动态创建自定义比较器的方法,模拟了实际业务开发中常见的“按特定字段组合进行去重、合并与比较”的复杂场景。熟练掌握这些方法,能极大提升数据处理代码的简洁性和效率。
通过本文对 Distinct, Union, Intersect, Except 的详细拆解和实战演练,你应该已经掌握了 LINQ 集合关系操作的核心。理解其延迟执行特性和自定义比较规则的方法,是灵活运用的关键。在实际开发中,结合 .NET 6 及更高版本的新 *By 系列方法,能让你的代码更加优雅高效。如果你想深入探讨更多 C# 或 LINQ 的高级用法,欢迎到 云栈社区 的技术论坛板块,与更多开发者交流学习心得和实战经验。更多系统性的技术文档和教程也可以在社区的知识库中找到。