Для чего нужно ключевое слово yield в C# и как оно работает?
Что такое yield и как он работает?
Ключевое слово yield в C# используется для создания итераторов — методов, которые возвращают последовательность значений по одному, не загружая всю коллекцию в память. Это реализация паттерна "ленивые вычисления" (lazy evaluation). Вместо того чтобы вернуть готовый список, метод с yield "замораживает" своё состояние после каждого yield return и продолжает выполнение при следующем обращении.
Ключевая разница: жадные vs ленивые вычисления
Ваш первый метод с List<int> — это жадный подход: он вычисляет все числа, помещает их в список и возвращает готовый результат. Второй метод — ленивый: он вычисляет числа по требованию.
Рассмотрим пример, где это критично. Допустим, вам нужно получить первые 10 чисел Фибоначчи из миллиона:
// Жадный подход — вычислит ВСЁ сразу
var allNumbers = GetFibonacci(1_000_000); // Память: 4 МБ (int × 1 млн)
var first10 = allNumbers.Take(10); // Всё равно уже всё в памяти
// Ленивый подход — вычислит только 10 чисел
var first10Lazy = GetFibonacciLazy(1_000_000).Take(10); // Память: минимум
С yield вы не выделяете память под весь массив. Кроме того, если потребитель (например, foreach) прервёт итерацию досрочно (через break), генерация чисел остановится.
Как yield работает под капотом?
Компилятор C# превращает метод с yield в государственную машину (state machine). Он создаёт вложенный класс, который реализует интерфейсы IEnumerable<T> и IEnumerator<T>. Этот класс хранит:
- Текущее состояние (на какой строке кода остановились)
- Локальные переменные (
a,b,i) - Текущий возвращаемый элемент
Пример сгенерированного кода (упрощённо):
private sealed class <GetFibonacci>d__0 : IEnumerable<int>, IEnumerator<int>
{
private int <>1__state;
private int <>2__current;
private int count;
private int a;
private int b;
private int temp;
private int i;
bool MoveNext()
{
switch (<>1__state)
{
case 0: // Начало метода
<>1__state = -1;
a = 0; b = 1;
i = 0;
goto case 1;
case 1: // Цикл for
if (i < count)
{
<>2__current = a;
<>1__state = 2;
return true;
}
break;
case 2: // После yield return
temp = a;
a = b;
b = temp + b;
i++;
goto case 1;
}
return false;
}
}
Когда использовать yield?
- Бесконечные последовательности (например, генератор случайных чисел)
- Обработка больших данных (чтение строк из файла, пагинация)
- Цепочки LINQ — методы вроде
Where(),Select()внутри используютyield - Отложенные вычисления — когда результат может не понадобиться полностью
Когда НЕ использовать yield?
- Если нужно многократно обходить коллекцию — каждый вызов
GetEnumerator()создаёт новую итерацию - Если данные уже есть в памяти (список, массив) —
yieldдобавит издержки - Если метод должен выбрасывать исключения при создании, а не при итерации
Практический пример: yield break
Есть ещё форма yield break — она завершает итерацию досрочно:
public IEnumerable<int> GetPrimes(int max)
{
for (int i = 2; i <= max; i++)
{
if (IsPrime(i))
yield return i;
if (i > 1000) // Ограничим сверху
yield break;
}
}
Таким образом, yield — это мощный инструмент для создания эффективных итераторов, который позволяет писать чистый, декларативный код без лишних коллекций.
Альтернативный взгляд: yield в асинхронном коде
Стоит упомянуть, что в C# есть ещё await foreach и yield return в асинхронных методах (начиная с C# 8.0). Это позволяет создавать асинхронные последовательности — IAsyncEnumerable<T>.
Пример — чтение строк из большого файла:
public async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
using var reader = new StreamReader(path);
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
yield return line; // Асинхронный yield
}
}
// Использование:
await foreach (var line in ReadLinesAsync("data.txt"))
{
Console.WriteLine(line);
}
Это особенно полезно при работе с сетевыми запросами, базами данных или потоковой обработкой, где каждый элемент может приходить с задержкой. Вместо того чтобы ждать загрузки всех данных, вы обрабатываете их по мере поступления.
Также отмечу, что yield нельзя использовать в методах с модификаторами ref или out, в try-catch (только в try-finally), и в анонимных методах/лямбдах. Это ограничения, связанные с генерацией state machine.
Ответов пока нет.
Войдите чтобы оставить ответ