CodeGym /课程 /C# SELF /LINQ里的 Union、 Intersect、 Except 操作

LINQ里的 Union、 Intersect、 Except 操作

C# SELF
第 33 级, 课程 3
可用

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.Union(B)
所有在 A 或 B 里的(唯一元素)。
A.Intersect(B)
同时在 A 和 B 里的。
A.Except(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)来加速。

2
任务
C# SELF, 第 33 级, 课程 3
已锁定
使用 Intersect 来找出共同的元素
使用 Intersect 来找出共同的元素
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION