1. Wprowadzenie
Przypomnijmy sobie, jak działa zwykły Select:
var names = users.Select(user => user.Name);
Każdemu użytkownikowi przypisujemy jego imię — dostajemy listę imion. Proste! A co jeśli chcemy dostać listę wszystkich zamówień wszystkich użytkowników?
var ordersList = users.Select(user => user.Orders);
Co się tutaj stanie? Dla każdego użytkownika dostaniemy listę zamówień. Czyli wynikiem będzie kolekcja kolekcji (IEnumerable<List<Order>>). To jak półka z pudełkami: żeby dostać się do każdego zamówienia, musisz ręcznie otwierać pudełko (listę zamówień użytkownika) za pudełkiem.
Chcemy cały ten zbiór "rozpakować", żeby pracować z nim jak ze zwykłą listą zamówień. I tu właśnie przychodzi z pomocą SelectMany.
Analogia: rozpakowywanie pudełek
Wyobraź sobie, że wszystkie twoje rzeczy nie leżą pomieszane, tylko w skrzynkach. Każda skrzynka — to kolekcja, np. zakupy jednego użytkownika. Jeśli chcesz zobaczyć WSZYSTKIE rzeczy naraz (np. do globalnej inwentaryzacji), nie będziesz przeglądać najpierw wszystkich skrzynek, a w środku — rzeczy, i tak w kółko. Dużo wygodniej wysypać zawartość wszystkich skrzynek na jedną wielką kupę — i dopiero ją przeglądać. Tak właśnie działa SelectMany.
2. Co to jest SelectMany?
SelectMany — to LINQ-metoda, która bierze kolekcję kolekcji i zamienia ją w jedną "płaską" kolekcję. Przez "płaską" rozumiemy, że wynikowa sekwencja nie będzie już zawierać wewnętrznych list — wszystkie elementy z zagnieżdżonych kolekcji znajdą się na jednym poziomie, jakby je "wyciągnąć" na zewnątrz. Żadnych matrioszek, po prostu jeden długi łańcuch wartości.
Sygnatura metody wygląda tak:
IEnumerable<TResult> SelectMany<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TResult>> selector
)
Działa to prosto: masz kolekcję (np. listę użytkowników), a każdy element zawiera w sobie inną kolekcję (np. listę zamówień tego użytkownika). SelectMany bierze każdego użytkownika, wyciąga jego zamówienia i łączy je w jeden wspólny strumień zamówień — bez "opakowania", bez zagnieżdżenia. To właśnie sprawia, że jest wygodny, gdy trzeba przejść od "listy list" do zwykłej listy.
3. Proste przykłady
Przykład 1: Lista użytkowników i ich zamówienia
Zacznijmy od naszego modelu aplikacji, który stopniowo rozwijamy:
// Załóżmy, że te klasy już są z poprzednich lekcji
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; }
}
Załóżmy, że mamy listę użytkowników:
var users = new List<User>
{
new User
{
Name = "Ivan",
Orders = new List<Order>
{
new Order { Id = 1, ProductName = "Książka" },
new Order { Id = 2, ProductName = "Długopis" }
}
},
new User
{
Name = "Maria",
Orders = new List<Order>
{
new Order { Id = 3, ProductName = "Laptop" }
}
}
};
Zadanie: uzyskać listę wszystkich produktów, które kiedykolwiek zostały kupione przez wszystkich użytkowników.
Zwykły Select:
var ordersPerUser = users.Select(user => user.Orders);
// IEnumerable<List<Order>>
Tutaj na wyjściu mamy wiele "pudełek", w każdym — lista zamówień danego użytkownika.
SelectMany:
var allOrders = users.SelectMany(user => user.Orders);
// IEnumerable<Order>
Teraz dostajemy jedną "długą" kolekcję wszystkich zamówień!
Wizualizacja
| Użytkownik | Zamówienia |
|---|---|
| Ivan | Książka, Długopis |
| Maria | Laptop |
- Po Select: [[Książka, Długopis], [Laptop]] — lista list
- Po SelectMany: [Książka, Długopis, Laptop] — jedna lista
Przykład 2: Praca z wieloma poziomami zagnieżdżenia
Jeśli np. zamówienie ma listę produktów (koszyk), wtedy SelectMany można użyć kilka razy — jak matrioszka!
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 = "Piotr",
Orders = new List<Order>
{
new Order
{
Id = 10,
Products = new List<Product>
{
new Product { Name = "Telefon" },
new Product { Name = "Ładowarka" }
}
}
}
}
};
Aby uzyskać wszystkie produkty ze wszystkich zamówień wszystkich użytkowników, używamy podwójnego SelectMany:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// IEnumerable<Product>
W ten sposób rozpakowujesz najpierw jeden poziom "pudełek", potem kolejny.
Przykład 3: Użycie SelectMany w Query Syntax
Jeśli bardziej lubisz SQL-owy styl LINQ (Query Syntax), to SelectMany wyraża się przez kilka from pod rząd:
var allProducts =
from user in users
from order in user.Orders
from product in order.Products
select product;
Tutaj każdy kolejny from bierze element z kolekcji zwróconej przez poprzedni.
users
└─ user1
└─ order1
└─ product1, product2
└─ order2
└─ product3
└─ user2
└─ order3
└─ product4
Query Syntax przechodzi przez wszystkie ścieżki i zwraca jedną długą listę produktów.
4. Warianty użycia i niuanse
SelectMany do przekształcania i filtrowania
W SelectMany możesz nie tylko rozpakowywać, ale też jednocześnie filtrować lub projektować elementy. Na przykład, wybrać tylko produkty droższe niż 1000 euro ze wszystkich zamówień:
var expensiveProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products)
.Where(p => p.Price > 1000);
Albo — z lekkim włożeniem logiki bezpośrednio do SelectMany (przez dodatkowy selektor):
var expensiveProducts = users
.SelectMany(u => u.Orders.SelectMany(o => o.Products))
.Where(p => p.Price > 1000);
To kwestia gustu: oba warianty dzielą problem na osobne, kolejne kroki.
Użycie przeciążenia SelectMany z projekcją
Dodatkowe "zaawansowane" przeciążenie metody pozwala nie tylko rozpakowywać, ale też od razu formować wynik. Na przykład: uzyskać pary "Imię użytkownika — Nazwa produktu":
var userProductPairs = users.SelectMany(
user => user.Orders.SelectMany(order => order.Products),
(user, product) => new { user.Name, product.Name }
);
Jak to działa? Pierwszy parametr — user => user.Orders.SelectMany(order => order.Products) — rozpakowuje wszystkie produkty użytkownika, drugi — łączy oryginalny obiekt użytkownika i produkt wewnątrz zagnieżdżonego enumeratora.
5. Ważne momenty i częste błędy
Możesz się spotkać z sytuacją, gdy zapomniałeś dodać SelectMany i zamiast płaskiej sekwencji dostałeś tablicę "pudełek". Na przykład, gdy użyłeś zwykłego Select, a nie SelectMany tam, gdzie trzeba rozpakować od razu wszystkie elementy. W efekcie nie da się przeiterować lub przetworzyć elementów bezpośrednio.
Bardzo łatwo się pogubić, jeśli kolekcje są zagnieżdżone, szczególnie przy dużej głębokości. Zasada jest prosta: jeśli na wyjściu nie chcesz listy list, tylko płaską listę — śmiało używaj SelectMany.
Gdzie się przyda w praktyce?
W każdym zadaniu, gdzie masz obiekt-kolekcję, a każdy jej element ma swoją kolekcję (np. "użytkownik — zamówienia", "kurs — studenci", "news — komentarze"), będziesz wracać do SelectMany. Na rozmowach kwalifikacyjnych to jeden z "ulubionych" tematów: nie każdy początkujący odróżnia Select od SelectMany i potrafi poprawnie rozpakować kolekcje.
W prawdziwych projektach pozwala to pisać ładny, zwięzły i szybki kod bez zbędnego poziomu zagnieżdżenia. Na platformie .NET to standardowy i zoptymalizowany sposób na obsługę "warstwowych" kolekcji.
Różnica między Select a SelectMany
Metoda Select zachowuje strukturę: jeśli używasz jej na kolekcji użytkowników, z których każdy ma listę zamówień, to wynikiem będzie kolekcja kolekcji — np. [[A, B], [C]]. To wygodne, jeśli chcesz dalej pracować z danymi grupami (np. po użytkownikach).
A SelectMany robi "spłaszczenie" — łączy wszystkie zagnieżdżone kolekcje w jedną wspólną sekwencję: [A, B, C]. To przydatne, gdy nie zależy ci na grupowaniu i chcesz pracować ze wszystkimi wewnętrznymi elementami jako jedną masą — np. znaleźć wszystkie produkty, niezależnie od tego, do którego użytkownika czy zamówienia należą.
+---------------------+ +--------------------+
| users | | allOrders |
+---------------------+ +--------------------+
| Ivan: [A, B] | --+ +-> Order A |
| Maria: [C] | --+ | +-> Order B |
+---------------------+ | | +-> Order C |
| | +--------------------+
Select: | |
[[A, B], [C]] <--------+ +
|
SelectMany: <--------+
[A, B, C]
Mówiąc wprost:
- Select → „daj mi listy zamówień po użytkownikach”;
- SelectMany → „daj mi po prostu wszystkie zamówienia”.
GO TO FULL VERSION