# Поглиблений C# LINQ у 2026: Оператори, Продуктивність та Питання на Співбесідах > Опануйте поглиблені оператори LINQ, відкладене виконання та оптимізацію продуктивності. Охоплює GroupBy, SelectMany, оптимізацію запитів та поширені питання на співбесідах для .NET розробників. - Published: 2026-08-23 - Updated: 2026-08-23 - Author: Anthony Fillion-Maillet - Tags: C#, LINQ, .NET, Продуктивність, Співбесіда - Reading time: 12 min --- Поглиблені оператори C# LINQ відрізняють досвідчених .NET розробників від початківців. Тоді як `Where` та `Select` обробляють базову фільтрацію, реальні проєкти вимагають вільного володіння `GroupBy`, `SelectMany`, патернами відкладеного виконання та композицією запитів з урахуванням продуктивності. > **Сигнал для Співбесіди** > > Інтерв'юери часто просять кандидатів пояснити різницю між відкладеним та негайним виконанням або оптимізувати повільний LINQ-запит. Розуміння того, що відбувається під капотом, відрізняє старших спеціалістів від тих, хто лише запам'ятовує синтаксис. ## Розуміння Відкладеного Виконання в LINQ LINQ-запити не виконуються при оголошенні. Виконання відбувається, коли результат перебирається, зазвичай через `foreach`, `ToList()` або `ToArray()`. Ця поведінка, яка називається відкладеним виконанням (deferred execution), дозволяє композицію запитів без повторного сканування бази даних або колекції. ```csharp // DeferredExecutionDemo.cs var numbers = new List { 1, 2, 3, 4, 5 }; // Query is defined but not executed var query = numbers.Where(n => n > 2); // Adding an element after query definition numbers.Add(6); // Execution happens here, includes 6 foreach (var n in query) { Console.WriteLine(n); // Output: 3, 4, 5, 6 } ``` Запит захоплює посилання на `numbers`, а не його вміст. Коли `foreach` виконує ітерацію, він бачить оновлену колекцію. Цей патерн корисний для поступової побудови динамічних запитів, але спричиняє помилки, коли розробники очікують поведінки знімка. Щоб примусити негайне виконання, слід викликати `ToList()`, `ToArray()` або `ToDictionary()`. Ці методи перебирають джерело один раз і кешують результати. ## GroupBy: За Межами Базового Групування `GroupBy` перетворює плоскі колекції в ієрархічні структури. Оператор приймає до чотирьох параметрів: селектор ключа, селектор елемента, селектор результату та користувацький компаратор. ```csharp // OrderAnalytics.cs public record Order(int Id, string Customer, decimal Amount, DateTime Date); var orders = GetOrders(); // Group by customer with monthly breakdown var customerMonthlyTotals = orders .GroupBy( o => new { o.Customer, Month = new DateTime(o.Date.Year, o.Date.Month, 1) }, (key, group) => new { key.Customer, key.Month, Total = group.Sum(o => o.Amount), OrderCount = group.Count() }) .OrderBy(x => x.Customer) .ThenBy(x => x.Month); // Output: Customer "Acme" spent $12,500 across 8 orders in August 2026 ``` Складений ключ групує як за клієнтом, так і за місяцем. Селектор результату проєктує безпосередньо у форму, готову для аналітики, уникаючи другого проходу `Select`. > **Зауваження щодо Пам'яті** > > `GroupBy` повинен буферизувати все джерело для формування груп. Для великих наборів даних або потокових сценаріїв варто розглянути групування на стороні бази даних з EF Core або використовувати `ToLookup`, коли згрупований результат буде доступний кілька разів. ## SelectMany: Вирівнювання Вкладених Колекцій `SelectMany` вирівнює відношення один-до-багатьох в єдину послідовність. Оператор обробляє сценарії, де кожен елемент створює нуль або більше результатів. ```csharp // ProductCatalog.cs public record Category(string Name, List Products); public record Product(string Name, decimal Price); var categories = GetCategories(); // Flatten all products with their category names var allProducts = categories .SelectMany( category => category.Products, (category, product) => new { CategoryName = category.Name, ProductName = product.Name, product.Price }); // Filter after flattening var expensiveProducts = allProducts .Where(p => p.Price > 100) .OrderByDescending(p => p.Price); ``` Двопараметрове перевантаження `SelectMany` надає доступ як до батьківського, так і до дочірнього елементів, дозволяючи проєкції, що поєднують дані з обох рівнів. Цей патерн замінює вкладені цикли `foreach` декларативним підходом. Синтаксис запитів пропонує альтернативу, яку деякі розробники вважають більш читабельною для складних з'єднань: ```csharp // QuerySyntaxFlatten.cs var productsWithCategories = from category in categories from product in category.Products where product.Price > 100 select new { category.Name, product }; ``` Обидва варіанти компілюються в той самий виклик `SelectMany`. Вибір залежить від конвенцій команди та складності запиту. ## Оптимізація Продуктивності LINQ Абстракції LINQ мають свою вартість. Кожен зчеплений оператор створює об'єкт ітератора, а лямбда-вирази додають витрати на виклик делегатів. Для гарячих шляхів, що обробляють тисячі елементів на секунду, ці витрати накопичуються. ### Уникнення Множинних Перебирань ```csharp // MultipleEnumerationProblem.cs IEnumerable GetExpensiveOrders(IEnumerable orders) { var filtered = orders.Where(o => o.Amount > 1000); // WARNING: Enumerates twice if orders is not materialized if (!filtered.Any()) return Enumerable.Empty(); return filtered.OrderBy(o => o.Date); } ``` Якщо `orders` походить із запиту до бази даних або файлового потоку, цей код виконує джерело двічі. Слід матеріалізувати за допомогою `ToList()` перед множинними операціями: ```csharp // FixedMultipleEnumeration.cs IEnumerable GetExpensiveOrders(IEnumerable orders) { var filtered = orders.Where(o => o.Amount > 1000).ToList(); if (filtered.Count == 0) return Enumerable.Empty(); return filtered.OrderBy(o => o.Date); } ``` ### Вибір Правильного Оператора Деякі LINQ-оператори мають оптимізовані реалізації для конкретних типів колекцій: | Операція | Повільна Версія | Швидка Версія | Примітки | |-----------|--------------|--------------|-------| | Підрахунок | `collection.Count()` | `collection.Count` | Використовуйте властивість для `ICollection` | | Доступ до елемента | `collection.ElementAt(5)` | `collection[5]` | Використовуйте індексатор для `IList` | | Перевірка існування | `collection.Count() > 0` | `collection.Any()` | `Any()` виходить раніше | | Перший збіг | `collection.Where(x).First()` | `collection.First(x)` | Один прохід vs. два ітератори | ## Aggregate: Користувацькі Операції Згортання `Aggregate` будує єдиний результат з послідовності шляхом застосування функції акумулятора. Тоді як `Sum`, `Max` та `Average` обробляють типові випадки, `Aggregate` дозволяє користувацькі згортання. ```csharp // CustomAggregation.cs var transactions = new List { new("Deposit", 1000), new("Withdrawal", -200), new("Deposit", 500), new("Fee", -25) }; // Running balance with audit trail var balanceHistory = transactions.Aggregate( new List<(string Op, decimal Balance)>(), (history, tx) => { var previousBalance = history.Count > 0 ? history[^1].Balance : 0; history.Add((tx.Type, previousBalance + tx.Amount)); return history; }); // Output: [(Deposit, 1000), (Withdrawal, 800), (Deposit, 1300), (Fee, 1275)] ``` Трипараметрове перевантаження додає початкове значення та фінальний селектор результату, що корисно, коли тип акумулятора відрізняється від типу результату. ## LINQ to Objects vs. LINQ to Entities Розуміння того, де виконується запит, визначає його характеристики продуктивності. LINQ to Objects працює в пам'яті з делегатами C#. LINQ to Entities (EF Core) перекладає вирази в SQL. ```csharp // EFCoreQueryOptimization.cs using var context = new AppDbContext(); // BAD: Loads all orders, filters in memory var badQuery = context.Orders .ToList() .Where(o => o.Amount > 1000); // GOOD: Filters in database, loads only matching rows var goodQuery = context.Orders .Where(o => o.Amount > 1000) .ToList(); ``` Розміщення `ToList()` змінює все. У поганому прикладі SQL Server повертає тисячі рядків, потім C# відкидає більшість із них. У хорошому прикладі речення `WHERE` виконується на стороні сервера. Для складних проєкцій EF Core 9 (актуальний станом на серпень 2026) підтримує [розділені запити](https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries), щоб уникнути картезіанського вибуху при завантаженні пов'язаних колекцій. ## Поширені Питання на Співбесідах про LINQ Технічні співбесіди часто перевіряють розуміння LINQ через концептуальні питання та живе кодування. Наступні сценарії регулярно з'являються на співбесідах для .NET розробників. ### "Яка різниця між IEnumerable та IQueryable?" `IEnumerable` працює з делегатами в пам'яті. Кожен зчеплений оператор створює ітератор, який обробляє елементи по одному. Середовище виконання не може оптимізувати між операторами. `IQueryable` працює з деревами виразів. Весь запит компілюється у структуру даних, яку постачальник (наприклад, EF Core) перекладає в SQL. Це дозволяє фільтрацію, сортування та проєкцію на стороні сервера. ```csharp // IQueryableDemo.cs // Expression tree, translates to SQL WHERE clause IQueryable dbQuery = context.Orders.Where(o => o.Amount > 1000); // Delegate, executes in C# memory IEnumerable memoryQuery = orders.Where(o => o.Amount > 1000); ``` Слід обирати `IQueryable` при запитах до зовнішніх джерел даних. `IEnumerable` підходить для колекцій у пам'яті або коли запит повинен використовувати функції, доступні лише в C#, як-от регулярні вирази чи користувацькі методи. ### "Як знайти дублікати в колекції?" Це питання з кодування тестує `GroupBy` та фільтрацію: ```csharp // FindDuplicates.cs var emails = new[] { "a@test.com", "b@test.com", "a@test.com", "c@test.com" }; var duplicates = emails .GroupBy(e => e, StringComparer.OrdinalIgnoreCase) .Where(g => g.Count() > 1) .Select(g => g.Key); // Output: ["a@test.com"] ``` Рішення групує за самим елементом, фільтрує групи з більш ніж одним членом та проєктує ключ. Параметр `StringComparer.OrdinalIgnoreCase` обробляє порівняння електронних адрес без урахування регістру. ### "Як реалізувати пагінацію з LINQ?" ```csharp // Pagination.cs public IEnumerable GetProductsPage(int pageNumber, int pageSize) { return context.Products .OrderBy(p => p.Id) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToList(); } ``` Речення `OrderBy` є обов'язковим для детермінованої пагінації. Без нього механізми баз даних можуть повертати рядки у довільному порядку, що призводить до появи елементів на кількох сторінках або їх повного зникнення. Більше [питань для співбесіди з C#](/technologies/dotnet/interview-questions/csharp-advanced-features) на SharpSkill, що охоплює розширені функції, включаючи зіставлення шаблонів, записи та nullable reference types. ## Написання Запитів LINQ, Легких для Підтримки Читабельність запитів швидко погіршується, коли ланцюжки перевищують п'ять-шість операторів. Слід виділяти проміжні результати у названі змінні або розбивати складну логіку на окремі методи. ```csharp // MaintainableLINQ.cs public IEnumerable GenerateReports(IEnumerable orders) { var ordersByCustomer = GroupOrdersByCustomer(orders); var reportsWithMetrics = CalculateCustomerMetrics(ordersByCustomer); return ApplyBusinessRules(reportsWithMetrics); } private IEnumerable> GroupOrdersByCustomer( IEnumerable orders) { return orders .Where(o => o.Status == OrderStatus.Completed) .GroupBy(o => o.CustomerId); } private IEnumerable CalculateCustomerMetrics( IEnumerable> groups) { return groups.Select(g => new CustomerReport { CustomerId = g.Key, TotalSpent = g.Sum(o => o.Amount), OrderCount = g.Count(), AverageOrderValue = g.Average(o => o.Amount) }); } ``` Ця структура робить кожну трансформацію тестованою ізольовано. Назви методів документують намір краще, ніж вбудовані коментарі. ## Ключові Висновки для .NET Розробників - Відкладене виконання затримує обчислення запиту до перебирання. Слід використовувати `ToList()` або `ToArray()` для матеріалізації результатів, коли потрібна поведінка знімка або коли множинні перебирання звертатимуться до джерела даних повторно. - `GroupBy` буферизує все джерело. Для великих наборів даних слід перенести групування до бази даних за допомогою EF Core, замість завантаження рядків у пам'ять. - `SelectMany` вирівнює вкладені колекції. Двопараметрове перевантаження надає доступ як до батьківських, так і до дочірніх елементів для комбінованих проєкцій. - `IQueryable` будує дерева виразів для перекладу постачальником. `IEnumerable` виконує делегати в пам'яті. Вибір неправильного інтерфейсу призводить до повного сканування таблиць. - Розміщення операторів має значення для [продуктивності EF Core](/blog/dotnet/ef-core-performance-best-practices). Слід розміщувати `Where`, `OrderBy` та `Take` перед `ToList()`, щоб виконувати фільтри на стороні сервера. - Питання на співбесідах часто тестують розуміння відкладеного виконання, виявлення дублікатів за допомогою `GroupBy` та розрізнення `IEnumerable` та `IQueryable`. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/dotnet/csharp-linq-advanced-operators-performance-2026