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.

1
Опитування
Vstup do LINQ, рівень 31, лекція 4
Недоступний
Vstup do LINQ
Osnovy LINQ: prosti zapyty ta filtratsiya danykh
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ