yield return в C# — зачем он нужен и чем отличается от обычного return списка?
Наткнулся на конструкцию 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 ответа
Разница в том, когда и сколько вычисляется. Первый метод сразу строит весь список в памяти и возвращает его целиком. Второй (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 незаменим.
Подчеркну ловушку, на которую многие натыкаются: тело метода с 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 это про ленивость.