1. 前言
當你只是在處理「平坦」的物件清單(比如我們之前課程裡的商品清單或用戶清單),一切都很簡單:filter、transform、sort 隨你玩。但在真實的商業應用、面試(甚至是作業!)裡,常常會遇到裡面還包著其他集合的集合。
比如說,有個 User 類別,裡面有訂單清單。或者 Product 類別,裡面有評論清單。現在問題來了:怎麼拿到所有用戶的所有訂單?或者把所有商品的所有評論都收集起來?還是你要拿到所有訂單裡的所有商品?
這種時候,單靠 Select,甚至 C# 的魔法都不夠力。這時候就要請出主角 — SelectMany,它能把「list of lists」變成單一「list」。
廚房比喻
如果你有個箱子,裡面還有箱子,箱子裡有餅乾,你的任務是把所有餅乾倒進一個大碗。你不會一個箱子一個箱子慢慢倒,而是直接把所有箱子裡的餅乾全倒進碗裡。
這就是 SelectMany 在做的事:它把「箱子裡的箱子」攤平成一個「碗」。
2. 回顧一下我們的應用範例
前幾堂課我們做了一個簡單的用戶和商品管理應用。這次我們要加點「巢狀」元素的集合。
假設我們有這些類別:
public class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public List<Product> Products { get; set; }
}
public class Product
{
public string Name { get; set; }
}
現在我們有一個用戶清單,每個用戶有訂單清單,每個訂單又有商品清單。
怎麼拿到所有用戶所有訂單裡的所有商品?
假設我們要把所有商品都攤平成一個「碗」。如果你直接用 Select,會怎樣?
List<User> users = ...; // 假設資料已經有了
var allOrders = users.Select(u => u.Orders);
變數 allOrders 的型別會是... IEnumerable<List<Order>>。你拿到的是「list of lists」,也就是一堆訂單集合。我們想要的是一條平坦的訂單清單。
再試一次:
var allProducts = users.Select(u => u.Orders)
.Select(oList => oList.Select(o => o.Products));
現在呢?你得到的是「list of lists of lists」!反而把結構搞更複雜了,完全沒簡化。
3. 解法:SelectMany 操作符
這就是 SelectMany 的威力。它的任務就是把巢狀集合「攤平」(flatten)成一個。
基本語法:
collection.SelectMany(item => item.內部集合)
我們這個例子:
var allOrders = users.SelectMany(u => u.Orders);
// 型別:IEnumerable<Order>
現在你就有所有用戶的所有訂單的單一序列了。
再深入一點!所有用戶所有訂單裡的所有商品:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// 型別:IEnumerable<Product>
如果要一行寫完?
可以這樣:
var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));
這串鍊建議還是分段讀,除非你是機器人。
4. 大範例來拆解
我們來做點測試資料,實際看看 SelectMany 怎麼運作。
var users = new List<User>
{
new User
{
Name = "愛麗絲",
Orders = new List<Order>
{
new Order
{
Id = 1,
Products = new List<Product>
{
new Product { Name = "餅乾" },
new Product { Name = "牛奶" },
}
},
new Order
{
Id = 2,
Products = new List<Product>
{
new Product { Name = "巧克力" }
}
}
}
},
new User
{
Name = "鮑勃",
Orders = new List<Order>
{
new Order
{
Id = 3,
Products = new List<Product>
{
new Product { Name = "咖啡" },
new Product { Name = "茶" }
}
}
}
}
};
把所有商品「倒進一個碗」
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
foreach (var product in allProducts)
{
Console.WriteLine(product.Name);
}
程式執行結果:
餅乾
牛奶
巧克力
咖啡
茶
每一步發生什麼事?
- users.SelectMany(u => u.Orders) — 把所有用戶的訂單攤平成一個訂單集合(現在有 3 筆訂單)。
- .SelectMany(o => o.Products) — 把所有訂單裡的商品攤平。
注意: 如果你用 Select,你會拿到集合的集合(例如 IEnumerable<List<Product>>)。用 SelectMany 就能拿到「平坦」的商品集合:IEnumerable<Product>。
5. 圖解
用 SelectMany 前
users (List<User>)
└── User #1
│ └── Orders (List<Order>)
│ ├── Order #1 -> Products (List<Product>)
│ └── Order #2 -> Products (List<Product>)
└── User #2
└── Orders (List<Order>)
└── Order #3 -> Products (List<Product>)
第一次 SelectMany(u => u.Orders) 後
IEnumerable<Order>: [Order #1, Order #2, Order #3]
第二次 SelectMany(o => o.Products) 後
IEnumerable<Product>: [餅乾, 牛奶, 巧克力, 咖啡, 茶]
6. 比較:Select vs SelectMany
| Select | SelectMany | |
|---|---|---|
| 回傳 | 集合的「集合」 | 平坦的集合 |
| 範例 | → |
→ |
| 用在 | 想保留巢狀結構時 | 想要一條平坦序列時 |
表格:什麼時候用哪個操作符
| 你要做什麼 | 用什麼 | 結果 |
|---|---|---|
| 拿到 list of lists(不攤平) | Select | |
| 拿到一條「平坦」序列 | SelectMany | |
| 拿到「父子」配對 | SelectMany + 結果 selector | |
| 只要轉換「平坦」集合的元素 | Select | |
7. 有用的小細節
Query Syntax(SQL 風格)
如果你喜歡寫得像 SQL 一樣漂亮 — LINQ 也可以用這種語法:
var allProducts = from user in users
from order in user.Orders
from product in order.Products
select product;
這段跟 SelectMany 鏈起來一樣,只是看起來像多層的「from」。
攤平成矩陣
最經典的 SelectMany 用法 — 處理二維陣列或 list of lists。
List<List<int>> matrix = new List<List<int>>
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5 },
new List<int> { 6 }
};
怎麼拿到所有數字的單一清單?
var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
Console.Write(value + " "); // 1 2 3 4 5 6
}
8. 進階語法
有時候用 SelectMany 的 overload,可以回傳內部元素同時帶外部資訊。
舉例:要拿到「用戶名 — 商品名」配對,每個訂單裡的每個商品都要。
var userProductPairs = users.SelectMany(
user => user.Orders.SelectMany(
order => order.Products,
(order, product) => new { UserName = user.Name, ProductName = product.Name }
)
);
foreach (var pair in userProductPairs)
{
Console.WriteLine($"{pair.UserName} 訂購了 {pair.ProductName}");
}
這裡 lambda 的第二個參數就是外部集合的元素。這樣就不會「丟失」外層資訊。
9. 實用場景
一堆訂單,一堆商品
你是網店工程師。想算所有客戶買過多少種不同商品?這樣寫:
var uniqueProductNames = users
.SelectMany(user => user.Orders)
.SelectMany(order => order.Products)
.Select(product => product.Name)
.Distinct();
foreach (var name in uniqueProductNames)
{
Console.WriteLine(name);
}
從階層攤平成平坦集合
你有公司部門清單,每個部門有員工。想拿到全公司所有員工清單:
var allEmployees = departments.SelectMany(d => d.Employees);
處理字串:把單字陣列攤平成字元
string[] words = { "hello", "world" };
var allChars = words.SelectMany(w => w.ToCharArray());
foreach (var c in allChars)
{
Console.Write(c + " "); // h e l l o w o r l d
}
10. 常見錯誤與細節
現在你已經被 SelectMany 的威力圈粉了,來提醒幾個常見錯誤。
最常見的 — 以為 Select 會給你「平坦」集合。其實只要內部 function 回傳集合,它就永遠給你集合的集合。結果你要寫雙層迴圈或被型別搞混。
第二個錯誤 — 沒注意到「攤平」會讓你失去外層資訊:比如你只處理商品時,根本不知道它是哪個訂單、哪個用戶的,除非你用 overload 帶 selector。
第三個細節 — 如果你的內部清單可能是 null,用 SelectMany 前要加防護:要嘛先 filter,要嘛換成空清單,不然你按 F5 之前就先拿到 NullReferenceException 了。
GO TO FULL VERSION