CodeGym /課程 /C# SELF /join (

join ( Join) 合併集合

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

1. 前言

有時候我們會遇到要處理兩個不同的集合,這兩個集合之間有某種共同點。比如說,我們有一個訂單清單,還有一個獨立的顧客集合。要怎麼知道哪個訂單屬於哪個顧客?這時候就需要兩個集合有共同的識別碼 —— 比如 userId

在關聯式資料庫裡,這種需求就是用 JOIN 操作來解決,它會根據相同的 key 把不同表格的資料合併起來。在 LINQ 裡也有一個類似的 operator —— 也叫 Join

想像一下有兩個表格:第一個是圖書館讀者清單,裡面有他們的識別碼,第二個是書籍訂單清單,每個訂單都標有 id(讀者的 id)。用 join 我們就可以把這兩個表格「黏」在一起,依 id 合併,最後得到「讀者 + 他的訂單」這種配對。

對了,如果你以為 join 只用在「資料庫」那你就錯過很多啦!在寫程式的時候,合併集合超常見,尤其是跟外部系統或結構化檔案(像 JSONXML 或會計部門的 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. 表格:JoinGroupBy + SelectMany 比較

操作 結果 使用場景
Join
扁平的配對清單 經典 SQL JOIN。每個配對(符合條件)就是一行。
GroupBy + SelectMany
有子集合的群組 想要拿到「顧客 + 他的所有訂單」這種「一對多」的結構。

新手常常在需要「顧客 → 訂單清單」這種結構時用 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 裡要用 GroupJoinSelectMany 組合。這個我們下堂課會講。經典的 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 合併顧客和訂單的圖解

從圖裡可以看到,每個顧客都會跟他所有的訂單配對 —— 前提是 key 一樣。

9. 常見錯誤跟小細節

最常見的錯誤之一就是把參數順序搞錯,尤其是兩個集合的 key 型別一樣時。這種情況編譯器或 LINQ 不會報錯,但結果可能是空的或怪怪的。

還有一個常見陷阱 —— 想用 Join 做「左合併」,讓所有顧客都出現在結果裡(就算沒訂單)。但經典的 Join 只會配對有對應的元素,不會包含沒有對應的。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION