CodeGym /課程 /C# SELF /多個來源合併:LINQ 裡的 SelectMany

多個來源合併:LINQ 裡的 SelectMany

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

1. 前言

回憶一下普通的 Select 怎麼用:

var names = users.Select(user => user.Name);

每個用戶對應到他的名字——得到一個名字清單。很簡單!但如果我們想拿到所有用戶的所有訂單清單呢?

var ordersList = users.Select(user => user.Orders);

這裡會發生什麼?對每個用戶我們會拿到訂單清單。也就是說,結果會是一個集合的集合(IEnumerable<List<Order>>)。就像一排箱子:你要拿到每個訂單,得一個一個打開箱子(用戶的訂單清單)。

我們想要把所有東西「拆箱」,這樣就能像操作普通訂單清單一樣操作它。這時 SelectMany 就派上用場啦。

比喻:拆箱子

想像一下,你的東西不是亂放,而是都放在箱子裡。每個箱子就是一個集合,比如一個用戶的購物清單。如果你想一次看所有東西(比如大盤點),你不會一個一個箱子翻,然後再翻裡面的東西。最方便的做法就是把所有箱子裡的東西倒成一大堆——然後再慢慢挑。這就是 SelectMany 的用法。

2. 什麼是 SelectMany?

SelectMany 是 LINQ 的一個方法,它會把一個集合的集合變成一個「扁平」的集合。這裡的「扁平」意思是,結果序列不會再有內部清單——所有巢狀集合裡的元素都被「拉」到同一層,好像直接拿出來一樣。沒有俄羅斯娃娃,只有一條長長的值串。

方法的簽名長這樣:


IEnumerable<TResult> SelectMany<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, IEnumerable<TResult>> selector
)

它的運作很簡單:你有一個集合(比如用戶清單),每個元素裡面還有另一個集合(比如這個用戶的訂單清單)。SelectMany 會把每個用戶的訂單都拿出來,合成一個訂單流——沒有「包裝」,沒有巢狀。這就是它在你想把「清單的清單」變成普通清單時超好用的原因。

3. 簡單範例

範例 1:用戶清單和他們的訂單

從我們逐步發展的應用模型開始:

// 假設這些 class 已經在前面課程寫過了
class User
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

class Order
{
    public int Id { get; set; }
    public string ProductName { get; set; }
}

假設有一個用戶清單:

var users = new List<User>
{
    new User
    {
        Name = "伊萬",
        Orders = new List<Order>
        {
            new Order { Id = 1, ProductName = "書" },
            new Order { Id = 2, ProductName = "筆" }
        }
    },
    new User
    {
        Name = "瑪麗亞",
        Orders = new List<Order>
        {
            new Order { Id = 3, ProductName = "筆電" }
        }
    }
};

任務:拿到所有用戶曾經買過的所有商品清單。

普通 Select:

var ordersPerUser = users.Select(user => user.Orders);
// IEnumerable<List<Order>>

這樣結果就是一堆「箱子」,每個箱子裡是這個用戶的訂單清單。

SelectMany:

var allOrders = users.SelectMany(user => user.Orders);
// IEnumerable<Order>

這樣我們就拿到一個「長長」的所有訂單集合啦!

視覺化

用戶 訂單
伊萬 書, 筆
瑪麗亞 筆電
  • Select 之後: [[書, 筆], [筆電]] — 清單的清單
  • SelectMany 之後: [書, 筆, 筆電] — 一個清單

範例 2:多層巢狀的操作

如果我們的訂單裡還有商品清單(購物車),那 SelectMany 可以用好幾次——就像拆俄羅斯娃娃一樣!

class Product
{
    public string Name { get; set; }
}

class Order
{
    public int Id { get; set; }
    public List<Product> Products { get; set; }
}

class User
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

var users = new List<User>
{
    new User
    {
        Name = "彼得",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 10,
                Products = new List<Product>
                {
                    new Product { Name = "手機" },
                    new Product { Name = "充電器" }
                }
            }
        }
    }
};

要拿到所有用戶所有訂單的所有商品,就用兩次 SelectMany:

var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);
// IEnumerable<Product>

這樣你先拆一層「箱子」,再拆下一層。

範例 3:Query Syntax 裡用 SelectMany

如果你比較喜歡 SQL 風格的 LINQ(Query Syntax),那 SelectMany 就是連著寫幾個 from:

var allProducts =
    from user in users
    from order in user.Orders
    from product in order.Products
    select product;

這裡每個 from 都是從上一個回傳的集合裡拿元素。


users
  └─ user1
        └─ order1 
             └─ product1, product2
        └─ order2
             └─ product3
  └─ user2
        └─ order3
             └─ product4
SelectMany 的巢狀集合結構圖

Query Syntax 會跑遍所有路徑,回傳一個長長的商品清單。

4. 用法變化和小細節

SelectMany 做轉換和過濾

SelectMany 不只能單純拆箱,還能同時過濾或投影元素。比如,從所有訂單裡挑出價格超過 1000 歐元的商品:

var expensiveProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products)
    .Where(p => p.Price > 1000);

或者——直接在 SelectMany 裡加點邏輯(用額外 selector):

var expensiveProducts = users
    .SelectMany(u => u.Orders.SelectMany(o => o.Products))
    .Where(p => p.Price > 1000);

這就看你喜歡哪種寫法:兩種都是把問題拆成連續步驟。

用 SelectMany 的進階 overload 做投影

進階一點的 overload 可以不只拆箱,還能直接組合結果。比如:拿到「用戶名—商品名」的配對:

var userProductPairs = users.SelectMany(
    user => user.Orders.SelectMany(order => order.Products),
    (user, product) => new { user.Name, product.Name }
);

這怎麼運作?第一個參數 user => user.Orders.SelectMany(order => order.Products) 是拆出所有用戶的商品,第二個參數則是在巢狀 enumerator 裡把原本的 user 跟 product 組合起來。

5. 重要提醒和常見錯誤

有時你會忘了加 SelectMany,結果不是扁平序列而是一堆「箱子」。比如你用了普通 Select,但其實應該要拆開所有元素。這樣你就不能直接遍歷或處理元素了。

巢狀集合很容易搞混,尤其層數多的時候。記住一個原則:如果你要的不是清單的清單,而是扁平清單——就直接用 SelectMany。

實務上什麼時候用得到?

只要你有一個物件集合,每個元素又有自己的集合(比如「用戶—訂單」、「課程—學生」、「新聞—留言」),你就會一直用到 SelectMany。面試時這也是常考題:很多初學者分不清 Select 跟 SelectMany,也不會正確拆集合。

實際專案裡,這能讓你的程式碼又簡潔又快,沒有多餘巢狀。在 .NET 平台這是處理「多層」集合的標準又高效的做法。

Select 跟 SelectMany 的差別

Select 會保留結構:如果你對有訂單清單的用戶集合用它,結果會是集合的集合——比如 [[A, B], [C]]。這很適合你還想分組處理資料(比如按用戶)。

但 SelectMany 會「壓平」——把所有巢狀集合合成一個序列:[A, B, C]。這很適合你不在乎分組,只想把所有內部元素當成一大堆來操作——比如找出所有商品,不管是哪個用戶或訂單的。


+---------------------+         +--------------------+
| users               |         | allOrders          |
+---------------------+         +--------------------+
| 伊萬: [A, B]        | --+   +-> Order A           |
| 瑪麗亞: [C]         | --+ | +-> Order B           |
+---------------------+   | | +-> Order C           |
                          | | +--------------------+
     Select:              | |
   [[A, B], [C]] <--------+ +
                          |
     SelectMany: <--------+
   [A, B, C]
Select 跟 SelectMany 差別的圖解

簡單說:

  • Select →「給我每個用戶的訂單清單」;
  • SelectMany →「給我所有訂單就好」。
2
任務
C# SELF, 等級 33, 課堂 2
上鎖
使用 SelectMany 來合併多個集合
使用 SelectMany 來合併多個集合
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION