1. Введение
Когда вы работаете только с "плоским" списком объектов (например, со списком наших товаров или пользователей из предыдущих лекций), всё довольно просто: фильтруем, преобразуем, сортируем. Но в реальных бизнес-приложениях, на собеседованиях (и даже в домашних заданиях!) часто встречаются коллекции, которые внутри содержат другие коллекции.
Например, есть класс User, у которого есть список заказов. Или, скажем, класс Product, у которого есть список отзывов. А теперь задача: получить список всех заказов всех пользователей? Или собрать все отзывы ко всем товарам? А если надо получить список всех товаров во всех заказах?
В таких ситуациях обычного Select и даже волшебной силы C# иногда не хватает. Здесь на сцену выходит герой — оператор SelectMany, превращающий "список списков" в просто "список".
Аналогия на кухне
Если бы у вас была коробка, в которой ещё коробки, а в них печеньки, и ваша задача — высыпать все печеньки в одну большую миску. Вы бы не стали брать каждую коробку и высыпать из неё по одной печеньке, а сразу бы взяли и все коробки, и все печеньки — в миску.
Вот этим и занимается SelectMany: он "расплющивает" коллекцию, превращая "коробку с коробками" в одну "миску" с содержимым.
2. Вспомним пример приложения
В прошлых лекциях мы создавали простое приложение для работы с пользователями и продуктами. Для этой лекции добавим коллекции "вложенных" элементов.
Допустим, у нас есть классы:
public class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public List<Product> Products { get; set; }
}
public class Product
{
public string Name { get; set; }
}
Пусть теперь у нас есть список пользователей, у каждого из которых есть список заказов, а у каждого заказа — список продуктов.
Как получить все продукты всех заказов всех пользователей?
Допустим, нам нужно составить общий перечень всех продуктов — без вложенности, просто в одну "миску". Если мы попробуем применить обычный Select, что получится?
List<User> users = ...; // предположим, что где-то уже есть данные
var allOrders = users.Select(u => u.Orders);
Тип переменной allOrders будет... IEnumerable<List<Order>>. Получили "список списков", то есть множество коллекций заказов. Мы бы хотели получить один плоский список всех заказов.
Можно попробовать ещё раз:
var allProducts = users.Select(u => u.Orders)
.Select(oList => oList.Select(o => o.Products));
Что теперь? Теперь у нас "список списков списков"! Получается, мы только усложняем структуру, а не упрощаем её.
3. Решение: оператор SelectMany
Вот тут и кроется вся мощь SelectMany. Его задача — "выпрямить" (flatten) вложенные коллекции в одну.
Общий синтаксис:
collection.SelectMany(item => item.КоллекцияВнутри)
В нашем случае:
var allOrders = users.SelectMany(u => u.Orders);
// Тип: IEnumerable<Order>
Теперь у нас просто последовательность всех заказов всех пользователей.
Давайте пойдём ещё глубже! Все продукты всех заказов всех пользователей:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// Тип: IEnumerable<Product>
А если в одну строку?
Да, можно и так:
var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));
Эту цепочку лучше читать по частям, особенно если вы не робот.
4. Разбираем на большом примере
Давайте создадим тестовые данные и наглядно посмотрим, как работает SelectMany.
var users = new List<User>
{
new User
{
Name = "Алиса",
Orders = new List<Order>
{
new Order
{
Id = 1,
Products = new List<Product>
{
new Product { Name = "Печенье" },
new Product { Name = "Молоко" },
}
},
new Order
{
Id = 2,
Products = new List<Product>
{
new Product { Name = "Шоколад" }
}
}
}
},
new User
{
Name = "Боб",
Orders = new List<Order>
{
new Order
{
Id = 3,
Products = new List<Product>
{
new Product { Name = "Кофе" },
new Product { Name = "Чай" }
}
}
}
}
};
"Высыпаем" все продукты в одну миску
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
foreach (var product in allProducts)
{
Console.WriteLine(product.Name);
}
Результат работы программы:
Печенье
Молоко
Шоколад
Кофе
Чай
Что происходит на каждом этапе?
- users.SelectMany(u => u.Orders) — берём всех пользователей и "разворачиваем" их заказы в одну коллекцию заказов (сейчас у нас 3 заказа).
- .SelectMany(o => o.Products) — разворачиваем список продуктов из всех заказов.
Внимание: Если бы мы использовали обычный Select, мы получили бы коллекцию коллекций (например, IEnumerable<List<Product>>). А с помощью SelectMany мы получаем "плоскую" коллекцию продуктов: IEnumerable<Product>.
5. Как это выглядит на схеме
До SelectMany
users (List<User>)
└── User #1
│ └── Orders (List<Order>)
│ ├── Order #1 -> Products (List<Product>)
│ └── Order #2 -> Products (List<Product>)
└── User #2
└── Orders (List<Order>)
└── Order #3 -> Products (List<Product>)
После первого SelectMany(u => u.Orders)
IEnumerable<Order>: [Order #1, Order #2, Order #3]
После второго SelectMany(o => o.Products)
IEnumerable<Product>: [Печенье, Молоко, Шоколад, Кофе, Чай]
6. Сравнение: Select и SelectMany
| Select | SelectMany | |
|---|---|---|
| Возвращает | Коллекцию "коллекций" | Плоскую коллекцию |
| Пример | → |
→ |
| Используется | Если нужно сохранить структуру вложенности | Если нужна одна плоская последовательность |
Таблица: когда какой оператор использовать
| Что нужно сделать | Что использовать | Результат |
|---|---|---|
| Получить список списков (не "разглаживать") | Select | |
| Получить одну "плоскую" последовательность | SelectMany | |
| Получить пары "родитель-дочерний элемент" | SelectMany с селектором результата | |
| Только преобразовать элементы "плоской" коллекции | Select | |
7. Полезные нюансы
Синтаксис Query Syntax (для любителей "SQL-вида")
Для особо ценящих выразительность — LINQ позволяет писать запросы и в синтаксисе, похожем на SQL:
var allProducts = from user in users
from order in user.Orders
from product in order.Products
select product;
Этот код делает то же самое, что и цепочка из SelectMany, только выглядит как многоуровневый "from".
"Разглаживание" матрицы
Классический пример применения SelectMany — работа с двумерным массивом или списком списков.
List<List<int>> matrix = new List<List<int>>
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5 },
new List<int> { 6 }
};
Как получить один список всех чисел?
var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
Console.Write(value + " "); // 1 2 3 4 5 6
}
8. Продвинутый синтаксис
Иногда, используя перегрузку SelectMany, можно возвращать не только элемент внутренней коллекции, но и информацию из внешней.
Вот пример: надо получить пары "Имя пользователя — Имя продукта" для каждого продукта в каждом заказе.
var userProductPairs = users.SelectMany(
user => user.Orders.SelectMany(
order => order.Products,
(order, product) => new { UserName = user.Name, ProductName = product.Name }
)
);
foreach (var pair in userProductPairs)
{
Console.WriteLine($"{pair.UserName} заказал {pair.ProductName}");
}
Здесь второй параметр лямбды — это элемент внешней коллекции. Очень полезно, чтобы не "терять" информацию из внешних уровней.
9. Практические сценарии
Множество заказов, множество товаров
Вы — разработчик интернет-магазина. Хотите посчитать, сколько уникальных товаров купили все клиенты? Вот:
var uniqueProductNames = users
.SelectMany(user => user.Orders)
.SelectMany(order => order.Products)
.Select(product => product.Name)
.Distinct();
foreach (var name in uniqueProductNames)
{
Console.WriteLine(name);
}
Формирование плоской коллекции из иерархии
У вас есть список отделов компании, у каждого отдела — сотрудники. Хотите получить список всех сотрудников по компании:
var allEmployees = departments.SelectMany(d => d.Employees);
Работа со строками: "разглаживание" массива слов в символы
string[] words = { "hello", "world" };
var allChars = words.SelectMany(w => w.ToCharArray());
foreach (var c in allChars)
{
Console.Write(c + " "); // h e l l o w o r l d
}
10. Типичные ошибки и нюансы
Теперь, когда вы вдохновились мощью SelectMany, самое время предупредить о нескольких часто встречающихся ошибках.
Самая популярная — ожидать, что Select даст "плоскую" коллекцию. На деле он всегда возвращает коллекцию коллекций, если внутренняя функция возвращает коллекцию. В итоге приходится писать двойные циклы или путаться в типах.
Вторая ошибка — не замечать, что "разглаживание" приводит к потере информации о внешнем элементе: например, если вы работаете только с продуктами, вы не узнаете, в каком заказе или у какого пользователя он был, если не используете перегрузку с селектором результата.
Третий нюанс — если ваши внутренние списки могут быть null, перед использованием SelectMany добавьте защиту: либо фильтрацию, либо замену на пустой список, иначе получите NullReferenceException даже быстрее, чем нажмёте F5.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ