← Все вопросы

yield return в C# — зачем он нужен и чем отличается от обычного return списка?

Задан 10 месяцев назад1.3к просмотров2 ответа
10

Наткнулся на конструкцию yield return и вообще не понял, что она делает. Я привык собирать результат в List и возвращать его, а тут какой-то yield, который возвращает по одному элементу и при этом метод как будто не заканчивается.

Вот два варианта, которые вроде делают одно и то же:

// привычный способ
IEnumerable<int> GetEvens(int n)
{
    var list = new List<int>();
    for (int i = 0; i < n; i++)
        if (i % 2 == 0) list.Add(i);
    return list;
}

// и какой-то странный
IEnumerable<int> GetEvensYield(int n)
{
    for (int i = 0; i < n; i++)
        if (i % 2 == 0) yield return i;
}

В чём разница, если результат одинаковый? Зачем вообще нужен yield?

2 ответа

18
✓ Принятый ответ — помог автору

Разница в том, когда и сколько вычисляется. Первый метод сразу строит весь список в памяти и возвращает его целиком. Второй (yield) создаёт ленивый итератор: элементы вычисляются по одному, по требованию, прямо во время перебора в foreach.

Когда выполнение доходит до yield return i;, метод как бы «ставится на паузу», отдаёт один элемент и запоминает, где остановился. На следующей итерации foreach он продолжает с того же места. Список целиком в памяти не хранится.

Это даёт два важных плюса:

// 1. Можно работать с бесконечной последовательностью
IEnumerable<int> Naturals()
{
    int i = 1;
    while (true)
        yield return i++;
}

var firstFive = Naturals().Take(5).ToList(); // 1,2,3,4,5 — и не зависнет

// 2. Ничего не считается, пока не начали перебирать
var evens = GetEvensYield(1000); // тут НИ ОДНОГО числа ещё не посчитано
foreach (var e in evens.Take(3)) // посчитаются только первые 3
    Console.WriteLine(e);

Итого: yield экономит память и позволяет лениво генерировать данные. Если коллекция маленькая и нужна целиком — разницы почти нет, но на больших/бесконечных потоках yield незаменим.

8

Подчеркну ловушку, на которую многие натыкаются: тело метода с yield не выполняется до начала перебора. Если положить в начало проверку аргументов, она не сработает, пока кто-нибудь не начнёт итерацию:

IEnumerable<int> Bad(int n)
{
    if (n < 0) throw new ArgumentException(); // НЕ бросится при вызове Bad(-1)!
    for (int i = 0; i < n; i++) yield return i;
}

var seq = Bad(-1); // исключения нет
foreach (var x in seq) {} // вот только здесь бросится

Поэтому валидацию аргументов в итераторах обычно выносят в отдельный обычный метод-обёртку. А в остальном принятый ответ всё верно объясняет — yield это про ленивость.

Ваш ответ

Войдите, чтобы ответить на вопрос.
Поддержать проект