Чудеса ключевого слова yield

Хорошо, когда я возился с созданием пользовательского счетчика, я заметил это поведение, которое касается выход

Скажем, у вас есть что-то вроде этого:

  public class EnumeratorExample 
  {

        public static IEnumerable<int> GetSource(int startPoint) 
        {
                int[] values = new int[]{1,2,3,4,5,6,7};
                Contract.Invariant(startPoint < values.Length);
                bool keepSearching = true;
                int index = startPoint;

                while(keepSearching) 
                {
                      yield return values[index];
                      //The mind reels here
                      index ++ 
                      keepSearching = index < values.Length;
                }
        }

  } 

Что позволяет под капотом компилятора выполнять индекс ++ и остальную часть кода в цикле while после того, как вы технически выполните возврат из функции?


person dexter    schedule 15.12.2010    source источник


Ответы (5)


Компилятор переписывает код в конечный автомат. Единственный метод, который вы написали, разделен на разные части. Каждый раз, когда вы вызываете MoveNext (неявно или явно), состояние изменяется и выполняется правильный блок кода.

Предлагаем прочитать, если вы хотите узнать больше деталей:

person Mark Byers    schedule 15.12.2010
comment
Да ладно, госмашина, это я читал. Но какой код он генерирует и что с ним делает конечный автомат? Псевдокод был бы очень признателен. - person dexter; 16.12.2010
comment
@Max Malygin: статья, на которую я дал ссылку csharpindepth.com/Articles/Chapter6/, показывает код, который генерируется. - person Mark Byers; 16.12.2010
comment
Также проверьте ответ Мэтта Грира: stackoverflow.com/questions/4455796/ Он также нашел часть 4 серии. - person Mark Byers; 16.12.2010
comment
Спасибо за крик. Эта ссылка может быть лучше, так как она дает их по порядку: blogs.msdn. com/b/ericlippert/archive/tags/iterators - person Eric Lippert; 16.12.2010

Компилятор генерирует конечный автомат от вашего имени.

Из спецификации языка:

10.14 Итераторы

10.14.4 Объекты перечислителя

Когда член функции, возвращающий тип интерфейса перечислителя, реализуется с использованием блока итератора, вызов члена функции не приводит к немедленному выполнению кода в блоке итератора. Вместо этого создается и возвращается объект перечислителя. Этот объект инкапсулирует код, указанный в блоке итератора, и выполнение кода в блоке итератора происходит при вызове метода MoveNext объекта перечислителя. Объект перечислителя имеет следующие характеристики:

• Он реализует IEnumerator и IEnumerator, где T — тип доходности итератора.

• Он реализует System.IDisposable.

• Он инициализируется копией значений аргументов (если они есть) и значением экземпляра, переданным члену функции.

• Он имеет четыре возможных состояния: «до», «выполняется», «приостановлено» и «после», и изначально находится в состоянии «до».

Объект перечислителя обычно является экземпляром класса перечислителя, сгенерированного компилятором, который инкапсулирует код в блоке итератора и реализует интерфейсы перечислителя, но возможны и другие методы реализации. Если класс перечислителя создается компилятором, этот класс будет прямо или косвенно вложен в класс, содержащий функцию-член, он будет иметь закрытый доступ и имя, зарезервированное для использования компилятором (§2.4.2). .

Чтобы получить представление об этом, вот как Reflector декомпилирует ваш класс:

public class EnumeratorExample
{
    // Methods
    public static IEnumerable<int> GetSource(int startPoint)
    {
        return new <GetSource>d__0(-2) { <>3__startPoint = startPoint };
    }

    // Nested Types
    [CompilerGenerated]
    private sealed class <GetSource>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
    {
        // Fields
        private int <>1__state;
        private int <>2__current;
        public int <>3__startPoint;
        private int <>l__initialThreadId;
        public int <index>5__3;
        public bool <keepSearching>5__2;
        public int[] <values>5__1;
        public int startPoint;

        // Methods
        [DebuggerHidden]
        public <GetSource>d__0(int <>1__state)
        {
            this.<>1__state = <>1__state;
            this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
        }

        private bool MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<values>5__1 = new int[] { 1, 2, 3, 4, 5, 6, 7 };
                    this.<keepSearching>5__2 = true;
                    this.<index>5__3 = this.startPoint;
                    while (this.<keepSearching>5__2)
                    {
                        this.<>2__current = this.<values>5__1[this.<index>5__3];
                        this.<>1__state = 1;
                        return true;
                    Label_0073:
                        this.<>1__state = -1;
                        this.<index>5__3++;
                        this.<keepSearching>5__2 = this.<index>5__3 < this.<values>5__1.Length;
                    }
                    break;

                case 1:
                    goto Label_0073;
            }
            return false;
        }

        [DebuggerHidden]
        IEnumerator<int> IEnumerable<int>.GetEnumerator()
        {
            EnumeratorExample.<GetSource>d__0 d__;
            if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
            {
                this.<>1__state = 0;
                d__ = this;
            }
            else
            {
                d__ = new EnumeratorExample.<GetSource>d__0(0);
            }
            d__.startPoint = this.<>3__startPoint;
            return d__;
        }

        [DebuggerHidden]
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
        }

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

        void IDisposable.Dispose()
        {
        }

        // Properties
        int IEnumerator<int>.Current
        {
            [DebuggerHidden]
            get
            {
                return this.<>2__current;
            }
        }

        object IEnumerator.Current
        {
            [DebuggerHidden]
            get
            {
                return this.<>2__current;
            }
        }
    }
}
person Ani    schedule 15.12.2010

Урожайность волшебная.

Ну не совсем. Компилятор создает полный класс для создания перечисления, которое вы делаете. Это в основном сахар, чтобы сделать вашу жизнь проще.

Прочтите это для вступления.

РЕДАКТИРОВАТЬ: Неправильно. Ссылка изменена, проверьте еще раз, если у вас был один раз.

person Donnie    schedule 15.12.2010
comment
@Max - В зависимости от того, когда вы перешли по ссылке, теперь она может отличаться. Я изначально разместил неправильный. - person Donnie; 16.12.2010

Вот отличная серия блогов (от ветерана Microsoft Рэймонда Чена), в которой подробно описывается, как работает yield:

часть 1: http://blogs.msdn.com/b/oldnewthing/archive/2008/08/12/8849519.aspx
часть 2: http://blogs.msdn.com/b/oldnewthing/archive/2008/08/13/8854601.aspx
часть 3: http://blogs.msdn.com/b/oldnewthing/archive/2008/08/14/8862242.aspx
часть 4: http://blogs.msdn.com/b/oldnewthing/archive/2008/08/15/8868267.aspx

person Matt Greer    schedule 15.12.2010

Это одна из самых сложных частей компилятора C#. Лучше всего прочитать бесплатную главу из книги Джона Скита C# in Depth (или лучше взять книгу и прочитать ее :-)

Простой способ реализации итераторов

Дополнительные пояснения см. в ответе Марка Гравелла здесь:

Может ли кто-нибудь демистифицировать ключевое слово yield?

person Dirk Vollmar    schedule 15.12.2010