CodeGym /課程 /C# SELF /LINQ 裡的 Union

LINQ 裡的 UnionIntersectExcept 操作

C# SELF
等級 33 , 課堂 3
開放

1. 前言

我們來聊一個超有趣、而且在實務上很常用的主題:LINQ 的多集合操作 — UnionIntersectExcept。這些方法讓你可以像在玩集合代數一樣操作集合(或者像在桌上數兩包貼紙一樣 — 其實差不多啦)。如果你沒聽過集合代數(或早就忘了),聽起來可能有點嚇人。其實超簡單:想像你有兩袋水果。集合代數就是讓你知道,哪些水果兩袋都有,哪些只有一袋有,還有如果把兩袋合起來會有哪些水果。就像玩貼紙一樣:可以全部加起來、找出重複的、或把一袋的從另一袋扣掉。就這麼簡單。

實戰: 這些方法超方便,像是你要合併兩個查詢的結果、找出共同元素、或是比對兩個集合的差異。例如:

  • 做出一份所有出現在銷售清單或採購清單裡的商品總表。
  • 找出兩邊都有的商品 — 也就是交集。
  • 或是找出倉庫裡有但沒賣出去的商品 — 有些東西真的放太久啦!

實際應用: 集合代數到處都用得到:像是根據訂閱過濾用戶、找出同時在不同群組的人、查找唯一或重複的訂單、比對兩個不同查詢的結果,還有很多很多。

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 會自動去掉重複(根據元素型別的 EqualsGetHashCode 實作)。如果你要合併自己的 class(像 Product),記得要正確實作這兩個方法,不然 Union 會怪怪的:明明一樣的東西卻被當成不同!

別忘了: 結果集合的順序會保留第一個集合的順序,新的元素會照它們在第二個集合第一次出現的順序加在後面。

物件範例

繼續我們的「商店」app。有兩個 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})");

常見錯誤: 如果沒 override Equals/GetHashCodeUnion 會把值一樣的不同實例當成不同元素!

3. Intersect 操作 — 交集

Intersect 會回傳所有集合裡都有的元素。這就是集合代數裡的「且」。

範例

還記得前面的清單嗎:

var commonProducts = warehouseProducts.Intersect(recentlySold);
foreach (var product in commonProducts)
{
    Console.WriteLine(product);
}
// 會印出:麵包, 起司

視覺化:

倉庫 賣出 Intersect (交集)
麵包 麵包 麵包
起司 起司 起司

什麼時候用得到?

  • 你想知道今天賣出去哪些倉庫裡有的商品。
  • 面試常問:「怎麼找兩個清單的交集?」 — LINQ 一行就搞定!
  • 商業 app 裡常常要用交集來做多條件過濾。

注意事項跟常見錯誤

如果某個元素在集合裡出現多次,結果只會有一個。
對於複雜物件(像 Product class),還是要正確實作 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. 視覺圖解與表格

來總結一下(假設有 AB 兩個清單):

操作 結果
A.Union(B)
所有在 AB 裡的(唯一元素)。
A.Intersect(B)
同時出現在 AB 的。
A.Except(B)
只在 A,不在 B 的。

圖解(如果能畫 Venn Diagram 就更棒了):


      [A]        [B]
      oooooooooo
      oooooooo oooo
      ooooo oo   ooo
      ooo    ooo  oo
      oo      o  ooo
      ooo    ooo ooo
  • Union:兩個圈圈裡所有東西。
  • Intersect:只有中間交集。
  • Except:只有 A 圈裡、沒碰到 B 的。

8. 用 UnionIntersectExcept 常見錯誤

錯誤 1:物件沒實作 EqualsGetHashCode
如果你的 class 沒 override 這兩個方法,UnionIntersectExcept 會亂搞:內容一樣的物件會被當成不同。結果你會拿到很奇怪(而且沒啥用)的結果。

錯誤 2:想只比對部分欄位卻沒用 IEqualityComparer
比如你只想比商品名稱,不想比整個物件,Intersect 不會自動懂你的意思。沒明確給 IEqualityComparer,結果一定跟你想的不一樣。

錯誤 3:以為元素順序會保留。
很多人以為結果集合會保留合併或交集的順序。但其實要看方法:Union 會保留第一個集合的順序,但 IntersectExcept 回傳的順序不一定。最好不要依賴順序。

錯誤 4:大集合沒考慮效能。
如果資料量很大,這些方法可能會很慢。可以先做聚合、過濾,或用 hash 結構(像 HashSet)來加速。

2
任務
C# SELF, 等級 33, 課堂 3
上鎖
使用 Intersect 找出共同元素
使用 Intersect 找出共同元素
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION