JavaRush /Курси /C# SELF /Об’єднання кількох джерел:

Об’єднання кількох джерел: SelectMany у LINQ

C# SELF
Рівень 33 , Лекція 2
Відкрита

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 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: Використання SelectMany у Query Syntax

Якщо вам більше до душі 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
Схема вкладених колекцій для SelectMany

Query Syntax проходить усі шляхи й повертає один довгий список продуктів.

4. Варіанти використання та тонкощі

SelectMany для перетворення й фільтрації

У SelectMany можна застосовувати не тільки просте розкриття, а й одночасну фільтрацію чи проекцію елементів. Наприклад, вибрати тільки продукти дорожчі за 1000 євро з усіх замовлень:

var expensiveProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products)
    .Where(p => p.Price > 1000);

Або — з невеликою частиною логіки безпосередньо всередині SelectMany (через додатковий селектор):

var expensiveProducts = users
    .SelectMany(u => u.Orders.SelectMany(o => o.Products))
    .Where(p => p.Price > 1000);

Це радше питання смаку: обидва варіанти ділять задачу на послідовні кроки.

Використання перевантаження SelectMany з проєкцією

Додаткове «розширене» перевантаження методу дає змогу не лише розкривати, а й одразу формувати результат. Наприклад: отримати пари «Імʼя користувача — назва продукту»:

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) — розкриває всі продукти користувача, другий — поєднує вихідний об’єкт користувача й продукт у межах вкладеного перелічувача.

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

Простіше кажучи:

  • Select → «дай мені списки замовлень по користувачах»;
  • SelectMany → «дай мені просто всі замовлення».
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ