CodeGym /Kursy /C# SELF /Łączenie wielu źródeł: Sel...

Łączenie wielu źródeł: SelectMany w LINQ

C# SELF
Poziom 33 , Lekcja 2
Dostępny

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
Schemat zagnieżdżonych kolekcji dla SelectMany

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]
Schemat różnicy między Select a SelectMany

Mówiąc wprost:

  • Select → „daj mi listy zamówień po użytkownikach”;
  • SelectMany → „daj mi po prostu wszystkie zamówienia”.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION