Правильное использование yield return в C#: когда и зачем?
Ключевое отличие: ленивое вычисление (lazy evaluation)
Основная разница между вашими примерами — в том, когда вычисляются элементы последовательности. В первом случае вы создаёте List<int>, заполняете его всеми числами сразу и возвращаете готовую коллекцию. Во втором — вы возвращаете итератор, который генерирует элементы по одному по мере запроса.
Когда использовать yield return
yield return идеален в следующих случаях:
- Бесконечные или очень большие последовательности — например, генерация чисел Фибоначчи, простых чисел или чтение строк из огромного файла. Вы не хотите хранить всё в памяти сразу.
- Ленивая фильтрация/трансформация — если вы комбинируете несколько операций LINQ (например,
Where,Select,Take), цепочка будет выполняться поэлементно, а не целиком. - Энергоэффективность — когда потребитель может прервать итерацию досрочно (например, через
breakилиTake(5)), вы не сгенерируете лишние элементы.
Пример: чтение строк из файла
public static IEnumerable<string> ReadLines(string path)
{
using (var reader = new StreamReader(path))
{
string line;
while ((line = reader.ReadLine()) != null)
{
yield return line; // строка читается только когда её запросят
}
}
}
Если файл размером 10 ГБ, первый подход (с List<string>) убьёт память. С yield вы обрабатываете строки по одной.
Когда НЕ стоит использовать yield return
- Если вам нужен случайный доступ по индексу —
yieldвозвращает толькоIEnumerable, а неIListили массив. Нельзя получитьcollection[5]без повторной итерации. - Если коллекция уже существует в памяти — нет смысла лениво возвращать элементы из
List<T>, проще вернуть его напрямую. - Если метод может выбросить исключение до начала итерации — с
yieldкод выполняется только при первом вызовеMoveNext(). Исключение будет отложено, что может быть неожиданным. - Если вы изменяете коллекцию во время итерации — итератор не отслеживает изменения исходного контейнера (в отличие от
List<T>, который выброситInvalidOperationException).
Подводные камни
- Многократная итерация — каждый вызов метода с
yieldсоздаёт новый итератор. Если вызватьGetFibonacci(10)дважды, вы получите два независимых набора чисел. Но если вы сохранили результат в переменную и проходите по нему дважды — последовательность будет сгенерирована дважды! - Побочные эффекты — не используйте
yieldв методах, которые изменяют глобальное состояние или выполняют дорогие операции при каждой итерации (например, запись в БД). Потребитель может не дойти до конца. - Нельзя использовать ref/out параметры — метод с
yieldне может иметьrefилиoutпараметров.
Итог
yield return — мощный инструмент для создания ленивых последовательностей. Используйте его, когда:
- Данные генерируются "на лету" (бесконечные ряды, чтение потоков)
- Потребитель может прервать итерацию досрочно
- Вы строите цепочки LINQ-операций
Избегайте его, когда нужен произвольный доступ, коллекция уже есть в памяти или важна строгая обработка ошибок на входе.
<<Альтернативный взгляд: производительность и отладка
Стоит добавить, что yield return создаёт под капотом целый класс-итератор (state machine). Это означает:
- Небольшой оверхед на создание объекта — для коротких последовательностей (до 10-20 элементов) явное создание
List<T>может быть быстрее, чем создание итератора. - Сложность отладки — в отладчике Visual Studio перемещение по стеку вызовов в методе с
yieldможет быть запутанным, так как реальный код выполняется в сгенерированном классе. - Альтернатива: возвращать массив или список — если данные статичны и невелики, просто верните
ArrayилиList. Это проще для чтения и отладки.
Практический совет: если вы не уверены, начните с yield return, но обязательно профилируйте в боевых условиях. Для 95% случаев разница несущественна, но для высоконагруженных систем выбор между ленивой и энергичной стратегией может быть критичен.