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) — раскрывает все продукты пользователя, второй — комбинирует исходный объект пользователя и продукт внутри вложенного enumerator-а.
5. Важные моменты и частые ошибки
Можно столкнуться с ситуацией, когда забыли добавить SelectMany и вместо плоской последовательности получили массив "коробок". Например, когда применили обычный Select, а не SelectMany там, где нужно раскрыть сразу все элементы. В результате нельзя перебрать или обработать элементы напрямую.
Очень легко запутаться, если коллекции вложены, особенно при большом уровне вложенности. Возьмите за правило: если на выходе нужен не список списков, а плоский список, — смело используйте 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 → «дай мне просто все заказы».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ