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
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 →「給我所有訂單就好」。
GO TO FULL VERSION