Влияет ли удаление элементов из листа после написания LINQ-запроса на результат запроса
Что проверяют
Знаете ли вы про отложенное выполнение LINQ и понимаете ли, что переменная с запросом хранит не результат, а описание вычисления. Следом часто просят объяснить, что будет, если менять список прямо во время перебора, и как то же самое ведёт себя с IQueryable в EF Core.
Как отвечать
Короткий ответ: да, влияет, если запрос ещё не материализован.
Where, Select, OrderBy, GroupBy, Skip, Take возвращают IEnumerable<T>, внутри которого лежит ссылка на исходный список и цепочка итераторов. Ничего не вычисляется, пока кто-то не начнёт перебор: foreach, ToList(), ToArray(), Count(), First(), Sum(), Any(). Поэтому:
- удалили до перебора:
var q = list.Where(x => x > 2); list.Remove(3);при перебореqтройки уже не будет; - материализовали раньше:
var snapshot = list.Where(x => x > 2).ToList();это отдельный список, дальнейшие удаления изlistего не касаются; - перебираете дважды: запрос выполнится дважды и может вернуть разное, если между проходами коллекция изменилась. Rider и ReSharper подсвечивают это как possible multiple enumeration;
- замыкания: переменная, захваченная лямбдой, тоже читается в момент перебора. Если после объявления запроса поменять
threshold, фильтр возьмёт новое значение.
Отдельный случай: удаление из List<T> внутри foreach по запросу над этим же списком. Итератор List<T> сверяет внутреннюю версию коллекции и бросает InvalidOperationException с текстом «Collection was modified». Решения: сначала ToList(), потом удаление, или сразу list.RemoveAll(predicate).
Для EF Core логика та же, только выполнение означает SQL-запрос, и каждый новый перебор IQueryable снова идёт в базу.
Где спотыкаются
Отвечают «нет, запрос уже написан, значит результат зафиксирован». Это верно только после ToList() или другого материализующего вызова.
Путают изменение содержимого списка и присваивание переменной. После numbers = new List<int>() запрос продолжит работать со старым списком: он захватил объект, а не имя.
Считают OrderBy немедленным. Он тоже отложенный, просто при первом переборе буферизует всю последовательность, чтобы отсортировать.