1. 入门
我们来聊一个很有意思、实际开发中经常用到的话题:LINQ的集合操作 —— Union、Intersect、Except。这些操作让你处理集合就像在玩集合代数(或者像程序员桌上数两包贴纸的数量——其实差不多)。如果你没听过集合代数(或者早忘了),听起来可能有点吓人。其实很简单:想象你有两个装水果的袋子。集合代数就是帮你搞清楚,哪些水果两个袋子都有,哪些只在一个袋子里,哪些合起来都有。就像小时候玩贴纸:可以全都放一起、找出重复的、或者把一堆里的从另一堆里剔除。就这么简单。
实战: 这些方法特别适合用在你要合并两个查询结果、找出共同元素、或者算出集合差异的时候。比如:
- 搞一个所有商品的总列表,不管是在销售清单还是采购清单里出现过的。
- 找出既在销售清单又在采购清单里的商品——也就是交集。
- 或者查查仓库里有但没人买的商品——肯定有点滞销!
实际场景: 集合代数到处都用得到:比如按订阅过滤用户、找出在不同群组里的人、查找唯一或重叠的订单、对比两个不同数据库查询的结果,等等等等。
2. Union 操作 —— 合并集合
很简单,直接上代码。假设我们有两个商品列表:
List<string> warehouseProducts = new List<string> { "牛奶", "面包", "奶酪", "鸡蛋" };
List<string> recentlySold = new List<string> { "面包", "奶酪", "萨拉米", "茶" };
Union 干嘛用的?
Union 会返回所有唯一的元素,只要在任意一个集合里出现过就行。
本质上就是合并。
var allProducts = warehouseProducts.Union(recentlySold);
foreach (var product in allProducts)
{
Console.WriteLine(product);
}
// 输出:牛奶, 面包, 奶酪, 鸡蛋, 萨拉米, 茶
直观点看是这样:
| 仓库 | 卖出 | Union (合并) |
|---|---|---|
| 牛奶 | 面包 | 牛奶 |
| 面包 | 奶酪 | 面包 |
| 奶酪 | 萨拉米 | 奶酪 |
| 鸡蛋 | 茶 | 鸡蛋 |
| 萨拉米 | ||
| 茶 |
用集合代数的说法,Union 就是“或”操作:把所有地方出现过的都给我。想象你有两盒不同口味的茶——你想尝遍所有口味,不管有没重复的茶——每种只喝一次。
Union 会自动去重(靠元素类型的 Equals 和 GetHashCode 方法实现)。如果你要合并自定义类(比如 Product),记得把这俩方法写对,不然 Union 会很迷:明明一样的元素也会当成不同的!
别忘了: 结果集合的顺序会保留第一个集合的顺序,新的元素会按它们在第二个集合第一次出现的顺序加到后面。
对象的例子
继续我们的“商店”小程序。有两个 Product 列表:
public class Product
{
public string Name { get; set; }
public string Category { get; set; }
// 为了让 Union/Intersect/Except 正常工作,Equals 和 GetHashCode 必须写对!
public override bool Equals(object? obj) =>
obj is Product other && Name == other.Name && Category == other.Category;
public override int GetHashCode() => HashCode.Combine(Name, Category);
}
List<Product> stock = new()
{
new Product { Name = "牛奶", Category = "乳制品" },
new Product { Name = "面包", Category = "面包类" },
};
List<Product> sold = new()
{
new Product { Name = "面包", Category = "面包类" },
new Product { Name = "萨拉米", Category = "香肠" },
};
var all = stock.Union(sold);
// 注意:元素不会重复,
// 就算“看起来”一样,只要逻辑上相等就只算一个。
foreach (var p in all)
Console.WriteLine($"{p.Name} ({p.Category})");
常见坑: 如果你没重写 Equals/GetHashCode,Union 会把值一样的对象当成不同的!
3. Intersect 操作 —— 取交集
Intersect 会返回所有集合里都出现过的元素。就是集合代数里的“与”操作。
例子
还记得上面的商品列表吧:
var commonProducts = warehouseProducts.Intersect(recentlySold);
foreach (var product in commonProducts)
{
Console.WriteLine(product);
}
// 输出:面包, 奶酪
直观图:
| 仓库 | 卖出 | Intersect (交集) |
|---|---|---|
| 面包 | 面包 | 面包 |
| 奶酪 | 奶酪 | 奶酪 |
啥时候用?
- 你想知道仓库里哪些商品今天卖出去了。
- 面试常问:“怎么找两个列表的交集?”——LINQ一行搞定!
- 业务系统里经常要按多个条件过滤数据,这时候交集就很有用了。
注意点和常见坑
如果集合里有重复元素,结果里只会有一个。
对于复杂对象(比如 Product 类),还是得写好 Equals/GetHashCode。
对象的例子
var commonObjects = stock.Intersect(sold);
foreach (var p in commonObjects)
Console.WriteLine($"{p.Name} ({p.Category})");
// 只会输出 面包 (面包类)
4. Except 操作 —— 做差集
Except 会返回只在第一个集合里有、第二个集合里没有的元素。
例子
var productsOnlyInStock = warehouseProducts.Except(recentlySold);
foreach (var product in productsOnlyInStock)
{
Console.WriteLine(product);
}
// 输出:牛奶, 鸡蛋
也就是说,这些商品还在仓库,但没卖出去。
直观图:
| 仓库 | 卖出 | Except (只在仓库) |
|---|---|---|
| 牛奶 | 牛奶 | |
| 鸡蛋 | 鸡蛋 | |
| 面包 | 面包 | (跳过) |
| 奶酪 | 奶酪 | (跳过) |
类比
就像你把已经发出去的文件从桌上的那一摞里拿掉——只剩下还在你手里的,别处没有的。
对象的例子
var unsold = stock.Except(sold);
foreach (var p in unsold)
Console.WriteLine($"{p.Name} ({p.Category})");
// 输出:牛奶 (乳制品)
! 不太直观的点
Except 对顺序很敏感:A.Except(B) 跟 B.Except(A) 完全不是一回事!第一个集合是“被减的”,第二个是“要去掉的”。
5. LINQ多操作组合
有时候一个操作不够用。比如你想找出只在仓库或只在卖出清单里有、但不会同时出现在两个集合里的商品(这叫“对称差集”)。
对称差集(集合的“XOR”):
var onlyInOne = warehouseProducts.Except(recentlySold)
.Union(recentlySold.Except(warehouseProducts));
foreach (var product in onlyInOne)
{
Console.WriteLine(product);
}
// 输出:牛奶, 鸡蛋, 萨拉米, 茶
复杂逻辑时LINQ组合用法:
// 找出既不在仓库也不在卖出清单,只在“预计到货”列表里的商品
List<string> expected = new() { "咖啡", "茶", "牛奶" };
var onlyExpected = expected.Except(warehouseProducts.Union(recentlySold));
foreach (var product in onlyExpected)
Console.WriteLine(product);
// 输出:咖啡
6. 自定义类型和 IEqualityComparer
有时候你不想按所有字段比较对象,比如只关心商品名,分类无所谓。这时LINQ方法支持一个额外参数——IEqualityComparer<T>,你可以自定义比较规则。
自定义比较器例子:
class ProductNameComparer : IEqualityComparer<Product>
{
public bool Equals(Product? x, Product? y) => x?.Name == y?.Name;
public int GetHashCode(Product obj) => obj.Name.GetHashCode();
}
var comp = new ProductNameComparer();
var uniqueByName = stock.Union(sold, comp);
foreach (var p in uniqueByName)
Console.WriteLine(p.Name); // 牛奶, 面包, 萨拉米
这个方法特别适合你不想全局改 Equals/GetHashCode,只是临时想按某种规则比较对象的时候。
7. 可视化图表和表格
总结下各种操作(假设有 A 和 B 两个列表):
| 操作 | 结果 |
|---|---|
|
所有在 A 或 B 里的(唯一元素)。 |
|
同时在 A 和 B 里的。 |
|
只在 A 里、不在 B 里的。 |
图示(如果能画Venn图就好了):
[A] [B]
oooooooooo
oooooooo oooo
ooooo oo ooo
ooo ooo oo
oo o ooo
ooo ooo ooo
- Union:两个圈里所有的。
- Intersect:只有中间重叠的部分。
- Except:只在A圈里、不包括和B重叠的。
8. 用 Union、Intersect、Except 时常见的坑
坑1:自定义对象没写 Equals 和 GetHashCode。
如果你的类没重写这俩方法,Union、Intersect、Except 这些方法会乱来:内容一样的对象会被当成不同的。结果往往很迷(而且没啥用)。
坑2:想只按部分字段比较对象但没用 IEqualityComparer。
比如你只想按商品名比较,不关心其它字段,Intersect 默认是不会懂的。没传 IEqualityComparer,结果肯定和你想的不一样。
坑3:以为元素顺序不会变。
很多人以为结果集合会保留合并或交集时的顺序。其实要看方法:Union 会保留第一个集合的顺序,但 Intersect 和 Except 返回的顺序就不一定了。最好别指望顺序。
坑4:大集合性能问题没注意。
数据量大的时候,这些方法可能会很慢。可以考虑先聚合、过滤,或者用哈希结构(比如 HashSet)来加速。
GO TO FULL VERSION