Как работает отложенное выполнение в LINQ
Что проверяют
Понимаете ли вы, что LINQ-запрос это описание, а не результат, и когда он на самом деле выполняется. Следом обычно спрашивают про многократное перечисление и про разницу между IEnumerable и IQueryable в EF Core.
Как отвечать
Вызов numbers.Where(n => n > 2) ничего не фильтрует: он возвращает объект-итератор, который помнит источник и предикат. Работа начинается при перечислении: foreach, ToList(), ToArray(), ToDictionary(), First(), Count(), Any(), Sum(). Операторы делятся на группы:
- отложенные потоковые:
Where,Select,Take,Skipотдают элементы по одному, по мере запроса; - отложенные буферизующие:
OrderBy,GroupBy,Reverseтоже ждут перечисления, но на первомMoveNext()читают весь источник; - немедленные: всё, что возвращает не последовательность, а значение или готовую коллекцию.
Следствия, которые нужно назвать:
- каждое перечисление выполняет запрос заново: два
foreachпо одному запросу дают двойную работу, а в EF Core два обращения к базе. IDE предупреждает об этом как о возможном многократном перечислении; - результат зависит от состояния на момент перечисления: элемент, добавленный в список после объявления запроса, попадёт в выборку, изменённая захваченная переменная тоже учтётся;
- исключения переезжают: ошибка в лямбде случится при перечислении, далеко от места, где запрос написан. Проверки аргументов на
nullв стандартных операторах при этом срабатывают сразу.
У IQueryable в EF Core запрос копит дерево выражений и переводится в SQL тоже при перечислении. Если вернуть IQueryable из метода и перечислить после Dispose контекста, получится ObjectDisposedException. А AsEnumerable() посреди цепочки переносит остаток фильтрации в память.
Где спотыкаются
Перечисляют запрос несколько раз и не понимают, откуда лишние обращения к базе. Лечится одним ToList() в нужном месте.
Меняют исходный список внутри foreach по запросу к нему и получают InvalidOperationException: коллекция изменена во время перечисления.
Вызывают ToList() на IQueryable раньше Where и тянут из базы всю таблицу, чтобы отфильтровать её в памяти.