JavaRush /Курси /C# SELF /Відкладене виконання

Відкладене виконання

C# SELF
Рівень 34 , Лекція 0
Відкрита

1. Вступ

Уявіть: ви написали довгий, вишуканий LINQ-запит і думаєте: «Готово, зараз усе обробиться!». А потім помічаєте, що нічого не відбувається, доки ви не порахуєте елементи або не перетворите результат на масив. Це не баг, а особливість.

У .NET LINQ використовує відкладене виконання: запит не стартує, поки ви реально не почнете витягати дані. Це як ледачий офіціант — не побіжить на кухню, поки не почує: «Принесіть їжу!», навіть якщо замовлення вже оформлене.

Така поведінка, відома як ледаче обчислення (Lazy Evaluation), робить LINQ особливо ефективним для роботи з великими, потенційно нескінченними або ресурсомісткими джерелами даних.

Відкладене виконання означає, що LINQ‑запит не виконується одразу після визначення. Він починає працювати лише тоді, коли ви справді починаєте перебирання або перегляд даних у колекції.

Ілюстративний приклад


var numbers = new List<int> { 1, 2, 3, 4, 5 };

// LINQ-запит
var query = numbers.Where(n =>{
    Console.WriteLine($"Перевіряємо {n}");
    return n % 2 == 0;
});
Console.WriteLine("Запит визначено, але числа не перевірялись!");

// Тільки зараз починається обхід!
foreach (var n in query)
{
    Console.WriteLine($"Знайшли парне: {n}");
}

Що відбудеться?
Коли ви виконаєте цей код, побачите, що до циклу foreach у консоль нічого не виводиться — навіть не перевіряються умови у вашому Where. Щойно ви почнете перебирання елементів (наприклад, через foreach), запит почне виконуватися.

Оце й є deferred execution: доки ви не попросите — нічого не відбувається!

2. Навіщо потрібно відкладене виконання?

Відкладене виконання робить код не лише елегантним, а й дуже ефективним. Навіщо виконувати якусь роботу наперед, якщо результат може взагалі не знадобитися? Це особливо важливо, коли ви маєте справу з великими колекціями або потоками даних, які ще тільки надходять — уявіть джерело, що може бути нескінченним. Завантажувати все це одразу в памʼять — просто нерозумно.

Крім того, відкладене виконання дозволяє вільно поєднувати й розширювати запити. Ви можете вибудовувати складні ланцюжки LINQ-операцій, не переймаючись, що вони одразу почнуть виконуватися. Усе станеться лише тоді, коли ви справді почнете отримувати дані — і ні хвилиною раніше.

Аналогія

Відкладене виконання — це як список покупок у телефоні: ви можете додавати й редагувати товари скільки завгодно, але до магазину йдете лише тоді, коли справді готові щось купити (і тільки тоді працюєте з цим списком).

3. Як це працює «під капотом»?

LINQ‑запити, які повертають IEnumerable<T>, зазвичай реалізовані через ітератори (yield return) або спеціальні ледачі конструкції. Щоразу, коли ви починаєте перебирання колекції (наприклад, через foreach або ToList()), запит запускається наново.

Важливе спостереження

Якщо ви зміните вихідну колекцію між визначенням запиту та його виконанням, нові або змінені дані також потраплять у результат.

Приклад:


var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1);
numbers.Add(4); // додали нове число
foreach (var n in query)
{
    Console.WriteLine(n); // виведе 2, 3, 4
}

Схема: коли виконується LINQ-запит?

Схема відкладеного виконання: запит «чекає» до реального використання

4. Приклади, де «ледачість» LINQ має значення

Ледача фільтрація


var bigNumbers = Enumerable.Range(1, 1_000_000_000)
    .Where(n => n % 123_456 == 0);

foreach (var n in bigNumbers.Take(5))
{
    Console.WriteLine(n);
}

Що тут відбувається?
Запит створює потенційно мільярдну послідовність, але реально відфільтрує й поверне лише 5 чисел. Все інше просто не буде пораховано й не займе памʼяті.

Вкладені запити й раннє завершення

Припустімо, у вас є список замовлень і товарів, і ви хочете отримати перші 5 замовлень, що містять певний товар.


var orders = GetBigOrderList(); // уявімо, що тут тисячі замовлень

var filtered = orders
    .Where(order => order.Products.Any(p => p.Name == "Кава"))
    .Take(5);

foreach(var o in filtered)
{
    Console.WriteLine(o.Id);
}

LINQ не розглядатиме інші замовлення, щойно знайде 5 збігів!

5. Несподіванки

Сценарії, у яких відкладене виконання може спричинити несподівані наслідки:

Зміна джерела даних після визначення запиту

Як показано вище, якщо між визначенням запиту та його реальним виконанням вихідна колекція зміниться, запит побачить нові дані.

Багаторазовий обхід одного запиту

LINQ‑запит виконується наново під час кожного перебирання.


var query = numbers.Where(n => {
    Console.WriteLine($"Перевіряємо {n}");
    return n % 2 == 0;
});

foreach(var n in query) {} // один обхід
foreach(var n in query) {} // другий обхід — знову все перерахується

Якщо вам потрібен повторюваний результат — матеріалізуйте дані (.ToList() або .ToArray()).

6. Коли запити LINQ не відкладені?

Не всі операції LINQ відкладені. Деякі методи виконують негайне («жадібне») виконання (Immediate Execution). Наприклад:

  • .ToList()
  • .ToArray()
  • .Count()
  • .Average()
  • .Sum()
  • .First(), .Last(), .Single()

Усі вони примушують LINQ виконати запит негайно, адже повертають не IEnumerable, а готовий результат.

Приклад:


var query = numbers.Where(n => n > 2);
var result = query.ToList(); // Тут запит одразу виконається!

7. Поняття «ледаче обчислення» (Lazy Evaluation)

Відкладене виконання (deferred execution) — це один із конкретних випадків ледачого обчислення (Lazy Evaluation) в .NET.

Ледаче обчислення — це коли результат не обчислюється, доки він реально не знадобиться. Окрім LINQ, у C# є й інші механізми для ледачих обчислень.

Клас Lazy<T>

C# має спеціальний тип Lazy<T>, щоб створювати значення «на вимогу».

Простий приклад:


// Створюємо ледаче число, яке рахуватиметься лише тоді, коли ви його запросите
var lazyValue = new Lazy<int>(() =>
{
    Console.WriteLine("Обчислюю значення!");
    return 42;
});

Console.WriteLine("Lazy-обʼєкт створено, але значення не обчислено");
Console.WriteLine($"Значення: {lazyValue.Value}"); // ось тут обчислення відбудеться

Навіщо це все?
Наприклад, ви зберігаєте параметр, який рідко потрібен, і його обчислення — тривале або ресурсомістке.

8. Таблиця: методи LINQ і режим виконання

Метод Відкладене виконання Негайне виконання
Where
Select
Take
Skip
Join
ToList
ToArray
Count
First

9. Типові помилки й особливості реалізації

«Зайва робота» через повторні обходи

Оскільки відкладене виконання призводить до нових обчислень під час кожного обходу, іноді можна отримати зайві повторні підрахунки.


var expensiveQuery = bigList.Where(x => SomeHeavyCalculation(x));

var result1 = expensiveQuery.ToList(); // порахували — раз
var result2 = expensiveQuery.ToList(); // порахували — два (той самий результат, але знову витрачаємо час)

Лікується матеріалізацією даних один раз:
var cached = expensiveQuery.ToList();

«Зловіть помилку, якщо зможете»

Якщо функція в запиті викидає виняток, він виникне лише під час обходу колекції, а не під час визначення запиту.

Модифікація колекції під час обходу

Зміна колекції під час її обходу може призвести до винятку InvalidOperationException.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ