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 實作)。如果你要合併自己的 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/GetHashCode,Union 會把值一樣的不同實例當成不同元素!
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. 視覺圖解與表格
來總結一下(假設有 A 跟 B 兩個清單):
| 操作 | 結果 |
|---|---|
|
所有在 A 或 B 裡的(唯一元素)。 |
|
同時出現在 A 跟 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. 用 Union、Intersect、Except 常見錯誤
錯誤 1:物件沒實作 Equals 跟 GetHashCode。
如果你的 class 沒 override 這兩個方法,Union、Intersect、Except 會亂搞:內容一樣的物件會被當成不同。結果你會拿到很奇怪(而且沒啥用)的結果。
錯誤 2:想只比對部分欄位卻沒用 IEqualityComparer。
比如你只想比商品名稱,不想比整個物件,Intersect 不會自動懂你的意思。沒明確給 IEqualityComparer,結果一定跟你想的不一樣。
錯誤 3:以為元素順序會保留。
很多人以為結果集合會保留合併或交集的順序。但其實要看方法:Union 會保留第一個集合的順序,但 Intersect 跟 Except 回傳的順序不一定。最好不要依賴順序。
錯誤 4:大集合沒考慮效能。
如果資料量很大,這些方法可能會很慢。可以先做聚合、過濾,或用 hash 結構(像 HashSet)來加速。
GO TO FULL VERSION