Имеет ли смысл материализовать ToList после изменения сигнатуры на IEnumerable?
Что проверяют
Понимаете ли вы отложенное выполнение LINQ и то, что тип в сигнатуре не превращает данные в коллекцию. Следом обычно спрашивают про многократное перечисление и про то, что будет, если за IEnumerable прячется запрос EF Core.
Как отвечать
Замена List<T> на IEnumerable<T> меняет контракт, а не данные. Нужен ли ToList(), зависит от того, что стоит за последовательностью.
Если это возвращаемое значение:
- данные уже в памяти: лишний
ToList()копирует за O(n) и выделяет память. Хотите запретить изменение снаружи, возвращайтеIReadOnlyList<T>илиIReadOnlyCollection<T>, у них естьCount, а у первого и индексатор; - ленивый LINQ или
yield return: без материализации запрос выполнится у вызывающего, возможно не один раз и позже, чем вы ждёте. Исключения тоже вылетят при переборе, а не при вызове метода; - запрос EF Core:
IQueryableпод видомIEnumerableвыполнится, когда вызывающий начнёт перебор. ЕслиDbContextк этому моменту освобождён, будетObjectDisposedException. Здесь материализация внутри метода черезawait query.ToListAsync()обязательна.
Если IEnumerable<T> стал параметром:
- один проход: материализовать не нужно, в этом и смысл широкого типа;
- несколько проходов (
Count(), потомforeach, потомAny()): материализуйте один раз в начале, иначе каждый проход заново выполнит исходный запрос. Анализатор CA1851 предупреждает как раз о таком многократном перечислении.
Узнать количество без перебора, если источник его знает, позволяет TryGetNonEnumeratedCount (.NET 6 и новее).
Где спотыкаются
Отвечают «ToList() всегда плохо» или «всегда хорошо». Сильный ответ называет условие: где живут данные и сколько раз их перебирают.
Возвращают из репозитория ленивый запрос под IEnumerable и получают ошибку за пределами using, а в тестах с List в памяти всё работает.
Материализуют IQueryable до фильтрации: ToList() перед Where вытягивает всю таблицу, и фильтр работает уже в памяти приложения.