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
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 → «дай мені просто всі замовлення».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ