JavaRush /Курсы /C# SELF /Выравнивание коллекций с помощью

Выравнивание коллекций с помощью SelectMany

C# SELF
31 уровень , 4 лекция
Открыта

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);
}

Результат работы программы:

Печенье
Молоко
Шоколад
Кофе
Чай

Что происходит на каждом этапе?

  1. users.SelectMany(u => u.Orders) — берём всех пользователей и "разворачиваем" их заказы в одну коллекцию заказов (сейчас у нас 3 заказа).
  2. .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
Возвращает Коллекцию "коллекций" Плоскую коллекцию
Пример
users.Select(u => u.Orders)
IEnumerable<List<Order>>
users.SelectMany(u => u.Orders)
IEnumerable<Order>
Используется Если нужно сохранить структуру вложенности Если нужна одна плоская последовательность

Таблица: когда какой оператор использовать

Что нужно сделать Что использовать Результат
Получить список списков (не "разглаживать") Select
IEnumerable<ICollection<T>>
Получить одну "плоскую" последовательность SelectMany
IEnumerable<T>
Получить пары "родитель-дочерний элемент" SelectMany с селектором результата
IEnumerable<CustomType>
Только преобразовать элементы "плоской" коллекции Select
IEnumerable<TransformedType>

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.

2
Задача
C# SELF, 31 уровень, 4 лекция
Недоступна
Извлечение всех предметов из вложенных списков
Извлечение всех предметов из вложенных списков
1
Опрос
Введение в LINQ, 31 уровень, 4 лекция
Недоступен
Введение в LINQ
Основы LINQ: простые запросы и фильтрация данных
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ