Метод, использующий yield return и yield break, должен возвращать интерфейс IEnumerable. Проще говоря, результатом будет перечисление. Это позволяет использовать его, например, в конструкции foreach. При этом:

  • метод вызывается перед каждым проходом цикла для получения нового значения перечисления;
  • локальные переменные метода сохраняют текущие значения в течении всех итераций цикла;
  • отсутствие возвращаемого значения или вызов yield break завершают перечисление и сам цикл.


Данной информации достаточно для того, чтобы начать использовать yield. В качестве примера получим значения степеней от 1 до exponent для заданного числа number:

namespace YieldKeywordDemo
{
    using System;
    using System.Collections;
 
    public class PowerList
    {
        public IEnumerable Power(int number, int exponent)
        {
            int valueCounter = 0;
            int currentResult = 1;
            while (valueCounter++ < exponent) {
                currentResult = currentResult * number;
                yield return currentResult;
            }
        }
    }
 
    class Program
    {
        static void Main(string[] args)
        {
            PowerList demoObj = new PowerList();
  
            // Display powers of 2 up to the exponent 10
            foreach (int i in demoObj.Power(2, 10)) {
                Console.Write("{0} ", i);
            }
 
            Console.ReadKey(true);
        }
    }
}

После запуска на консоль будет выделен ряд чисел: 2 4 8 16 32 64 128 256 512 1024. На этом краткое описание можно закончить и начать писать свои методы с использованием yield.

Детали реализации

Если вам интересно как работает yield и почему сохраняются переменные, то перейдем к деталям его реализации. Посмотрим что получилось в результате работы компилятора.

Небольшая подсказка: обратите внимание, что внутренние переменные метода сохраняют значения между его вызовами. Это должно натолкнуть на мысль, что они не могут быть в стеке. А значит должен появиться новый объект для, как минимум, хранения их в куче. Еще не догадались?
ispolzovanie-yeld

Загрузим полученный EXE файл проекта в .NET Reflector. Все оказывается достаточно просто. Компилятор создал вложенный private sealed класс, который реализуетIEnumerable. Параметры и внутренние переменные нашего метода стали его publicполями. Посмотрим результаты декомпиляции:

[CompilerGenerated]
private sealed class <Power>d__0 : IEnumerable<object>, IEnumerable,
                                   IEnumerator<object>, IEnumerator,
                                   IDisposable
{
    // Fields
    private int <>1__state;
    private object <>2__current;
    public int <>3__exponent;
    public int <>3__number;
    public PowerList <>4__this;
    private int <>l__initialThreadId;
    public int <currentResult>5__2;
    public int <valueCounter>5__1;
    public int exponent;
    public int number;
 
    // Methods
    [DebuggerHidden]
    public <Power>d__0(int <>1__state);
    private bool MoveNext();
    [DebuggerHidden]
    IEnumerator<object> IEnumerable<object>.GetEnumerator();
    [DebuggerHidden]
    IEnumerator IEnumerable.GetEnumerator();
    [DebuggerHidden]
    void IEnumerator.Reset();
    void IDisposable.Dispose();
 
    // Properties
    object IEnumerator<object>.Current { [DebuggerHidden] get; }
    object IEnumerator.Current { [DebuggerHidden] get; }
}

Код получения значений степени перемещен в метод MoveNext(). При этом, само значение сохраняется в свойстве Current. А в наш метод внесены небольшие изменения для его работы в рамках созданного класса.

private bool MoveNext()
{
    switch (this.<>1__state)
    {
        case 0:
            this.<>1__state = -1;
            this.<valueCounter>5__1 = 0;
            this.<currentResult>5__2 = 1;
            while (this.<valueCounter>5__1++ < this.exponent)
            {
                this.<currentResult>5__2 *= this.number;
                this.<>2__current = this.<currentResult>5__2;
                this.<>1__state = 1;
                return true;
            Label_0065:
                this.<>1__state = -1;
            }
            break;
 
        case 1:
            goto Label_0065;
    }
    return false;
}

Исходный метод Power() теперь используется для создания экземпляра внутреннего класса.

public IEnumerable Power(int number, int exponent)
{
    <Power>d__0 d__ = new <Power>d__0(-2);
    d__.<>4__this = this;
    d__.<>3__number = number;
    d__.<>3__exponent = exponent;
    return d__;
}

Как видим, в итоге компилятор практически создал за нас реализацию IEnumerable.

Разумеется, использование foreach не обязательно. Значения можно перебрать самостоятельно, используя свойство Current интерфейса IEnumerator. Например:

PowerList demoObj2 = new PowerList();
IEnumerable power = demoObj2.Power(2, 10);
IEnumerator e = power.GetEnumerator();
e.MoveNext();
int value = (int)e.Current;
Console.WriteLine("n{0}", value);

Но стоит запомнить, что перебрать значения можно только один раз. Причина в сгенерированном коде метода Reset(), который отвечает за установку перечислителя на первый элемент:

[DebuggerHidden]
void IEnumerator.Reset()
{
    throw new NotSupportedException();
}

Как видим, вызывать его бесполезно. Поэтому, если нам нужна возможность начинать перечисление сначала, то придется создавать класс и реализовывать IEnumerble самостоятельно.