1. 前言
有時候我們會遇到要處理兩個不同的集合,這兩個集合之間有某種共同點。比如說,我們有一個訂單清單,還有一個獨立的顧客集合。要怎麼知道哪個訂單屬於哪個顧客?這時候就需要兩個集合有共同的識別碼 —— 比如 userId。
在關聯式資料庫裡,這種需求就是用 JOIN 操作來解決,它會根據相同的 key 把不同表格的資料合併起來。在 LINQ 裡也有一個類似的 operator —— 也叫 Join。
想像一下有兩個表格:第一個是圖書館讀者清單,裡面有他們的識別碼,第二個是書籍訂單清單,每個訂單都標有 id(讀者的 id)。用 join 我們就可以把這兩個表格「黏」在一起,依 id 合併,最後得到「讀者 + 他的訂單」這種配對。
對了,如果你以為 join 只用在「資料庫」那你就錯過很多啦!在寫程式的時候,合併集合超常見,尤其是跟外部系統或結構化檔案(像 JSON、XML 或會計部門的 Excel 表)打交道時。
2. Join 方法簽名跟原理
這就是 Join 方法的長相:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // 第一個集合(外部)
IEnumerable<TInner> inner, // 第二個集合(內部)
Func<TOuter, TKey> outerKeySelector, // 怎麼從外部元素拿 key
Func<TInner, TKey> innerKeySelector, // 怎麼從內部元素拿 key
Func<TOuter, TInner, TResult> resultSelector) // 組合結果的 function(產生新元素)
一開始看起來有點複雜,不過我們馬上來拆解一下。
怎麼運作:
對於第一個集合(outer)的每個元素,LINQ 會去找第二個集合(inner)裡 key 相同的元素。key 一樣時就呼叫 resultSelector,然後把結果存進最後的集合。
3. 範例:合併顧客跟他們的訂單
我們先來建兩個 class 跟集合。這個想法就是我們課程裡的小專案(假裝我們在做一個小商店)。
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string Product { get; set; } = "";
}
// 顧客集合
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "瓦夏" },
new Customer { Id = 2, Name = "彼佳" },
new Customer { Id = 3, Name = "瑪莎" },
};
// 訂單集合
var orders = new List<Order>
{
new Order { Id = 101, CustomerId = 2, Product = "書" },
new Order { Id = 102, CustomerId = 1, Product = "原子筆" },
new Order { Id = 103, CustomerId = 2, Product = "筆記本" },
new Order { Id = 104, CustomerId = 3, Product = "橡皮擦" },
};
現在我們想要拿到所有顧客跟他們的訂單。比如說,輸出:"彼佳 訂購了 書"、"彼佳 訂購了 筆記本" 之類的。
來用 Join
// 依 customer.Id 跟 order.CustomerId 合併顧客和訂單
var query = customers.Join(
orders,
customer => customer.Id, // 怎麼從顧客拿 key
order => order.CustomerId, // 怎麼從訂單拿 key
(customer, order) => new // key 一樣時要做什麼(產生新物件)
{
CustomerName = customer.Name,
Product = order.Product
}
);
// 輸出結果
foreach (var item in query)
{
Console.WriteLine($"{item.CustomerName} 訂購了 {item.Product}");
}
會輸出:
瓦夏 訂購了 原子筆
彼佳 訂購了 書
彼佳 訂購了 筆記本
瑪莎 訂購了 橡皮擦
注意:如果一個顧客有多個訂單,他會在清單裡出現多次 —— 每個訂單一行。這就是預期的行為!
4. 表格:Join 跟 GroupBy + SelectMany 比較
| 操作 | 結果 | 使用場景 |
|---|---|---|
|
扁平的配對清單 | 經典 SQL JOIN。每個配對(符合條件)就是一行。 |
|
有子集合的群組 | 想要拿到「顧客 + 他的所有訂單」這種「一對多」的結構。 |
新手常常在需要「顧客 → 訂單清單」這種結構時用 Join。但 Join 是一行一行配對,不會幫你分群。這時候要用 GroupJoin(下堂課會講),或是 GroupBy。
5. Query Syntax(SQL 風格 LINQ)裡的 Join
LINQ 支援兩種語法:方法鏈(Method Syntax,像 Join(...))跟所謂的 SQL 風格(Query Syntax),看起來很像 SQL 查詢。
有些工程師 —— 尤其是以前寫過資料庫的人 —— 會覺得這種語法更直觀:
var query2 =
from customer in customers
join order in orders
on customer.Id equals order.CustomerId
select new
{
CustomerName = customer.Name,
Product = order.Product
};
foreach (var item in query2)
{
Console.WriteLine($"{item.CustomerName} 訂購了 {item.Product}");
}
注意:
在 Query Syntax 裡要用 equals 關鍵字 —— == 這裡不能用!
這是面試常見陷阱,尤其是新手 😉
6. 重要細節跟小陷阱
有時候不是所有集合裡的元素都會出現在結果裡。這是因為 Join 方法實作的是 「inner join」,也就是只有 key 一樣的元素才會被合併。如果某個顧客沒有訂單,他就不會出現在結果裡。同理,如果有訂單的 CustomerId 在顧客清單裡找不到,這個訂單也不會出現在結果裡。
那如果你想要所有顧客都出現(就算沒訂單)怎麼辦?這時候要用「左 join」,LINQ 裡要用 GroupJoin 加 SelectMany 組合。這個我們下堂課會講。經典的 Join 就是兩邊都要有對應 —— 不然就不會配對,元素就會被丟掉。
7. 多個 key(composite key)合併
有時候要依多個欄位合併集合。比如說:依「商品代碼 + 銷售年份」合併商品跟銷售紀錄。
LINQ 裡可以用匿名物件當 key 來解決:
var sales = ...; // 銷售紀錄
var products = ...; // 商品
var query = products.Join(
sales,
prod => new { prod.Code, prod.Year },
sale => new { sale.ProductCode, sale.Year },
(prod, sale) => new { prod.Name, sale.Amount }
);
重點是這些匿名物件的屬性名稱跟型別要一樣。如果不一樣,就算值一樣也不會配對成功。
8. 合併後的集合長怎樣 —— 圖解
這裡有個圖,簡單說明 join 的運作方式:
flowchart LR
subgraph Customers
A1["瓦夏 (Id=1)"]
A2["彼佳 (Id=2)"]
A3["瑪莎 (Id=3)"]
end
subgraph Orders
B1["原子筆 (CustomerId=1)"]
B2["書 (CustomerId=2)"]
B3["筆記本 (CustomerId=2)"]
B4["橡皮擦 (CustomerId=3)"]
end
A1-->|Id=1|B1
A2-->|Id=2|B2
A2-->|Id=2|B3
A3-->|Id=3|B4
從圖裡可以看到,每個顧客都會跟他所有的訂單配對 —— 前提是 key 一樣。
9. 常見錯誤跟小細節
最常見的錯誤之一就是把參數順序搞錯,尤其是兩個集合的 key 型別一樣時。這種情況編譯器或 LINQ 不會報錯,但結果可能是空的或怪怪的。
還有一個常見陷阱 —— 想用 Join 做「左合併」,讓所有顧客都出現在結果裡(就算沒訂單)。但經典的 Join 只會配對有對應的元素,不會包含沒有對應的。
GO TO FULL VERSION