1. Giới thiệu
Khi bạn chỉ làm việc với một list "phẳng" các object (ví dụ như list sản phẩm hoặc người dùng từ các bài trước), mọi thứ khá đơn giản: filter, transform, sort. Nhưng trong ứng dụng thực tế, phỏng vấn (và cả bài tập về nhà nữa!) thường gặp collection mà bên trong lại chứa collection khác.
Ví dụ, có class User có list đơn hàng. Hoặc class Product có list đánh giá. Giờ thử bài toán: lấy list tất cả đơn hàng của tất cả user? Hay gom hết đánh giá của mọi sản phẩm? Hoặc lấy list tất cả sản phẩm trong mọi đơn hàng?
Trong mấy trường hợp này, chỉ dùng Select hoặc thậm chí sức mạnh thần kỳ của C# cũng chưa đủ. Đây là lúc hero xuất hiện — operator SelectMany, biến "list các list" thành một "list" đơn giản.
So sánh với nhà bếp
Nếu bạn có một hộp, trong đó lại có các hộp nhỏ, trong các hộp nhỏ là bánh quy, và nhiệm vụ là đổ hết bánh quy vào một cái tô lớn. Bạn sẽ không lấy từng hộp nhỏ rồi đổ từng cái bánh quy, mà sẽ gom hết hộp nhỏ, bánh quy — vào tô luôn.
Đó chính là việc SelectMany làm: nó "làm phẳng" collection, biến "hộp chứa hộp" thành một "tô" chứa tất cả bên trong.
2. Nhớ lại ví dụ ứng dụng
Ở các bài trước, mình đã tạo một app đơn giản để làm việc với user và product. Cho bài này, mình sẽ thêm collection "lồng nhau".
Giả sử có các class:
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; }
}
Giờ ta có list user, mỗi user có list order, mỗi order lại có list product.
Làm sao lấy tất cả sản phẩm của mọi đơn hàng của mọi user?
Giả sử cần tạo một list tổng hợp tất cả sản phẩm — không lồng nhau, chỉ một "tô" thôi. Nếu thử dùng Select bình thường thì sao?
List<User> users = ...; // giả sử đã có data đâu đó
var allOrders = users.Select(u => u.Orders);
Kiểu của biến allOrders sẽ là... IEnumerable<List<Order>>. Tức là "list các list", nhiều collection order. Nhưng mình muốn một list phẳng tất cả order.
Thử tiếp lần nữa:
var allProducts = users.Select(u => u.Orders)
.Select(oList => oList.Select(o => o.Products));
Giờ thì sao? Giờ thành "list các list các list"! Càng làm càng phức tạp, không đơn giản hơn.
3. Giải pháp: operator SelectMany
Đây chính là sức mạnh của SelectMany. Nhiệm vụ của nó là "làm phẳng" (flatten) collection lồng nhau thành một.
Cú pháp tổng quát:
collection.SelectMany(item => item.CollectionBenTrong)
Trường hợp của mình:
var allOrders = users.SelectMany(u => u.Orders);
// Kiểu: IEnumerable<Order>
Giờ chỉ còn một chuỗi tất cả order của mọi user.
Đi sâu hơn nữa! Tất cả sản phẩm của mọi order của mọi user:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// Kiểu: IEnumerable<Product>
Có thể viết một dòng không?
Có luôn:
var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));
Đọc từng đoạn sẽ dễ hiểu hơn, trừ khi bạn là robot.
4. Phân tích ví dụ lớn
Tạo data test và xem tận mắt SelectMany hoạt động thế nào nhé.
var users = new List<User>
{
new User
{
Name = "Alysa",
Orders = new List<Order>
{
new Order
{
Id = 1,
Products = new List<Product>
{
new Product { Name = "Banh quy" },
new Product { Name = "Sua" },
}
},
new Order
{
Id = 2,
Products = new List<Product>
{
new Product { Name = "Socola" }
}
}
}
},
new User
{
Name = "Bob",
Orders = new List<Order>
{
new Order
{
Id = 3,
Products = new List<Product>
{
new Product { Name = "Ca phe" },
new Product { Name = "Tra" }
}
}
}
}
};
"Đổ" hết sản phẩm vào một tô
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
foreach (var product in allProducts)
{
Console.WriteLine(product.Name);
}
Kết quả chạy chương trình:
Banh quy
Sua
Socola
Ca phe
Tra
Chuyện gì xảy ra ở từng bước?
- users.SelectMany(u => u.Orders) — lấy tất cả user và "mở" order của họ thành một collection order (giờ có 3 order).
- .SelectMany(o => o.Products) — mở list product từ tất cả order.
Lưu ý: Nếu dùng Select bình thường, bạn sẽ nhận được collection các collection (ví dụ IEnumerable<List<Product>>). Còn với SelectMany thì ra collection "phẳng" các product: IEnumerable<Product>.
5. Sơ đồ minh họa
Trước 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>)
Sau lần đầu SelectMany(u => u.Orders)
IEnumerable<Order>: [Order #1, Order #2, Order #3]
Sau lần hai SelectMany(o => o.Products)
IEnumerable<Product>: [Banh quy, Sua, Socola, Ca phe, Tra]
6. So sánh: Select và SelectMany
| Select | SelectMany | |
|---|---|---|
| Trả về | Collection "collection" | Collection phẳng |
| Ví dụ | → |
→ |
| Dùng khi | Khi muốn giữ cấu trúc lồng nhau | Khi muốn một chuỗi phẳng |
Bảng: khi nào dùng operator nào
| Cần làm gì | Dùng gì | Kết quả |
|---|---|---|
| Lấy list các list (không làm phẳng) | Select | |
| Lấy một chuỗi "phẳng" | SelectMany | |
| Lấy cặp "cha-con" | SelectMany với result selector | |
| Chỉ transform phần tử của collection phẳng | Select | |
7. Một số lưu ý hay ho
Query Syntax (cho ai thích kiểu "SQL")
Ai thích code rõ ràng — LINQ cho phép viết query kiểu giống SQL:
var allProducts = from user in users
from order in user.Orders
from product in order.Products
select product;
Code này làm y chang chuỗi SelectMany, chỉ là nhìn như nhiều tầng "from".
"Làm phẳng" ma trận
Ví dụ kinh điển dùng SelectMany — làm việc với mảng 2 chiều hoặc list các list.
List<List<int>> matrix = new List<List<int>>
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5 },
new List<int> { 6 }
};
Làm sao lấy một list tất cả số?
var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
Console.Write(value + " "); // 1 2 3 4 5 6
}
8. Cú pháp nâng cao
Đôi khi, dùng overload của SelectMany, bạn có thể trả về không chỉ phần tử collection con mà còn info từ bên ngoài.
Ví dụ: lấy cặp "Tên user — Tên sản phẩm" cho mỗi sản phẩm trong mỗi order.
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} da dat {pair.ProductName}");
}
Ở đây tham số thứ hai của lambda là phần tử collection ngoài. Rất tiện để không "mất" info từ các tầng ngoài.
9. Tình huống thực tế
Nhiều đơn hàng, nhiều sản phẩm
Bạn là dev cho shop online. Muốn đếm có bao nhiêu sản phẩm unique mà tất cả khách đã mua? Đây:
var uniqueProductNames = users
.SelectMany(user => user.Orders)
.SelectMany(order => order.Products)
.Select(product => product.Name)
.Distinct();
foreach (var name in uniqueProductNames)
{
Console.WriteLine(name);
}
Tạo collection phẳng từ cấu trúc phân cấp
Bạn có list phòng ban công ty, mỗi phòng có nhân viên. Muốn lấy list tất cả nhân viên toàn công ty:
var allEmployees = departments.SelectMany(d => d.Employees);
Làm việc với string: "làm phẳng" mảng từ thành ký tự
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. Lỗi phổ biến và lưu ý
Giờ bạn đã thấy sức mạnh của SelectMany, cũng nên cảnh báo vài lỗi hay gặp.
Lỗi phổ biến nhất — tưởng Select sẽ trả về collection "phẳng". Thực tế nó luôn trả về collection các collection nếu hàm bên trong trả về collection. Kết quả là phải viết vòng lặp lồng hoặc rối kiểu dữ liệu.
Lỗi thứ hai — không để ý "làm phẳng" sẽ làm mất info về phần tử ngoài: ví dụ nếu chỉ làm việc với product, bạn sẽ không biết nó thuộc order nào, user nào nếu không dùng overload với result selector.
Lưu ý thứ ba — nếu list bên trong có thể là null, trước khi dùng SelectMany hãy thêm kiểm tra: filter hoặc thay bằng list rỗng, không thì sẽ dính NullReferenceException còn nhanh hơn cả bấm F5.
GO TO FULL VERSION