Avatar ·

Для чего нужно ключевое слово yield в C# и как оно работает?

Всем привет! Я недавно начал изучать C# и наткнулся на ключевое слово `yield`. Прочитал документацию на MSDN, но до конца не понял, зачем оно нужно. Вроде бы можно просто вернуть список или массив из метода, зачем использовать yield return? У меня есть такой код: я пишу метод, который генерирует последовательность чисел Фибоначчи. Сейчас я делаю это так: ```csharp public List GetFibonacci(int count) { var result = new List(); int a = 0, b = 1; for (int i = 0; i < count; i++) { result.Add(a); int temp = a; a = b; b = temp + b; } return result; } ``` Но я видел, что можно написать то же самое с `yield return`: ```csharp public IEnumerable GetFibonacci(int count) { int a = 0, b = 1; for (int i = 0; i < count; i++) { yield return a; int temp = a; a = b; b = temp + b; } } ``` В чем разница? Когда стоит использовать первый вариант, а когда второй? И что такое "ленивые вычисления"? Буду благодарен за понятное объяснение с примерами. <<>>

Что такое 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.