Отложенное выполнение
Конвейер из прошлых уроков устроен хитрее, чем кажется. Пока в конце ленты никто не берёт коробку, он стоит. Протянули руку — лента сдвинулась ровно на одну коробку, и все станции обработали только её.
Запрос LINQ работает так же. Строка с Where или Select ничего не вычисляет: она только собирает конвейер. Посмотрите, что из этого следует:
List<int> nums = [1, 2, 3];
var big = nums.Where(n => n > 1);
nums.Add(4);
Console.WriteLine(string.Join(" ", big)); // 2 3 4Число 4 добавили уже после строки с Where, а запрос его увидел. Когда выполнялась строка с Where, никакого отбора не было: запрос запомнил список и лямбду. Отбор случился в последней строке, когда string.Join начал перебирать запрос.
Запрос выполняется при переборе
Такое поведение называется отложенным выполнением: запрос LINQ выполняется не там, где написан, а когда его результат перебирают. Запрос перебирается в foreach, в string.Join, в методах вроде Count() и First(), а ещё в ToList() и ToArray().
В программе ниже лямбда для Select печатает строку при каждом вызове. Стрелка → nums.Select(…) у doubled значит, что переменная ведёт к запросу, а не к готовым числам. Нажимайте «Следующий шаг» и следите, когда появляются строки Select.
1List<int> nums = [1, 2];2var doubled = nums.Select(n =>3{4 Console.WriteLine($"Select {n}");5 return n * 2;6});7nums.Add(3);8Console.WriteLine("Query ready");9foreach (int d in doubled)10{11 Console.WriteLine(d);12}
Main создаёт список из двух чисел.
Пошаговый прогон показывает три вещи:
Query readyпечатается раньшеSelect 1,Select 2иSelect 3: строка сSelectлямбду не вызывала;- элементы идут по одному: лямбда обработала
1,foreachнапечатал2, и только потом очередь дошла до второго элемента; - число
3, добавленное после запроса, тоже прошло через лямбду: к моменту перебора оно уже лежало в списке.
Каждый перебор — заново
Запрос не запоминает свой результат. При каждом переборе он заново проходит по списку и заново вызывает лямбды. Если в программе выше перебрать doubled вторым foreach, строки Select 1, Select 2 и Select 3 напечатаются ещё раз.
Здесь аналогия с конвейером заканчивается. Коробка, снятая с ленты, уходит навсегда. А запрос можно перебрать сколько угодно раз, и каждый раз конвейер запустится с начала.
Снимок через ToList
Чтобы закрепить результат, запрос выполняют сразу:
List<int> nums = [1, 2, 3];
var big = nums.Where(n => n > 1);
List<int> snapshot = big.ToList();
nums.Add(4);
Console.WriteLine(string.Join(" ", big)); // 2 3 4
Console.WriteLine(string.Join(" ", snapshot)); // 2 3ToList() перебирает запрос сразу и складывает результат в новый список. Это снимок: после nums.Add(4) запрос big видит 4, а snapshot остался прежним.
Вызывайте ToList() или ToArray(), когда:
- нужен результат на этот момент, а исходный список потом изменится;
- результат придётся перебрать несколько раз, а лямбды не должны выполняться заново.
Отложенное выполнение на собеседовании
Этот вопрос часто задают на собеседовании: когда выполняется запрос LINQ? Ответ из двух частей:
Where,SelectиOrderByтолько строят запрос, а выполняется он при переборе;Count,Sum,First,Any,ToListиToArrayсразу начинают перебор, поэтому запрос выполняется в той же строке.
Отложенное выполнение — последняя тема курса. Потренироваться отвечать на такие вопросы можно в симуляторе собеседований IT Abyss.
Частая ошибка
Список меняли внутри foreach, который перебирает запрос по этому же списку:
List<int> nums = [1, 2, 3];
foreach (int n in nums.Where(x => x > 1))
{
nums.Add(n * 10); // ✗
}Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.Программа компилируется, но останавливается с исключением. Запрос отложенный, поэтому Where читает nums прямо во время цикла, а список изменили посреди перебора. Это та же ошибка, что возникает, если менять список в foreach по нему самому.
Исправление — снимок: foreach (int n in nums.Where(x => x > 1).ToList()). Цикл перебирает готовый список, и в nums без ошибки добавляются 20 и 30.
Главное
- Запрос LINQ выполняется не там, где написан, а когда его результат перебирают.
- Запрос видит список таким, каким тот стал к моменту перебора, и при каждом переборе выполняется заново.
ToList()иToArray()выполняют запрос сразу и сохраняют снимок, который не меняется вслед за списком.- Менять список, пока по нему перебирается запрос, нельзя: программа остановится с исключением
InvalidOperationException.