Pull to refresh

Реализация итераторов в C# (часть 1)

Reading time3 min
Views55K
Original author: Raymond Chen
От переводчика:
Не так давно мой менее опытный коллега спросил меня о том, для чего используется yield return в C#. Я не очень часто пишу свои итераторы, поэтому, отвечая ему, я сомневался в своих словах. Справившись в MSDN, я укрепился в сказанном, но у меня возник вопрос: “А во что же всё таки компилируется эта инструкция?” К тому моменту, я уже был знаком с переводимой статьёй, однако всё, что в ней сказано, давно “выветрилось”. Статья старая, но мне думается, что она может быть полезна для определённой группы разработчиков, привыкшей читать русскоязычные статьи и документы.

Ссылка на продолжение: реализация итераторов в C# (часть 2)



Как и анонимные методы, итераторы в C# являются сложным синтаксическим сахаром. Вы можете реализовать их полностью самостоятельно (в конце концов, в ранних версиях C# вам приходилось делать это), но использовать компилятор намного удобнее. Идея, стоящая за итераторами заключается в том, что они принимают функцию с yield return выражениями (и, возможно, yield break выражениями) и конвертируют её в конечный автомат. Когда вызывается yield return, состояние функции сохраняется, и при повторном обращении к итератору для получения очередного объекта это состояние восстанавливается. Главное в итераторах то, что все локальные переменные итератора (в том числе параметры итератора как предварительно инициализировнные локальные переменные, включая скрытый параметр this) становятся переменными-членами (далее полями) вспомогательного класса. Помимо этого вспомогательный класс содержит поле state, которое следит, где произошло прерывание исполнения, и поле current, хранящее самый последний из уже перечисленных объектов.

class MyClass {
    int limit = 0;
    public MyClass(int limit) { this.limit = limit; }
 
    public IEnumerable<int> CountFrom(int start)
    {
        for (int i = start; i <= limit; i++)
            yield return i;
    }
}

Метод CountFrom создаёт перечислитель целых чисел, который производит целые числа от start до limit включительно с шагом 1. Компилятор неявно конвертирует этот перечислитель во что-то вроде этого:
class MyClass_Enumerator : IEnumerable<int> {
     int state$0 = 0; // внутренний член
     int current$0; // внутренний член
     MyClass this$0; // неявный параметр CountFrom
     int start;     // явный параметр CountFrom
     int i;         // локальная переменная метода CountFrom
 
     public int Current {
         get { return current$0; }
     }
 
     public bool MoveNext()
     {
         switch (state$0) {
             case 0: goto resume$0;
             case 1: goto resume$1;
             case 2: return false;
         }
 
         resume$0:;
         for (i = start; i <= this$0.limit; i++) {
             current$0 = i;
             state$0 = 1;
             return true;
             resume$1:;
         }
         state$0 = 2;
         return false;
     }
 
     // ... прочее счетоводство, неважное здесь ...
}
 
public IEnumerable<int> CountFrom(int start)
{
     MyClass_Enumerator e = new MyClass_Enumerator();
     e.this$0 = this;
     e.start = start;
     return e;
}

Перечисляющий класс автоматически генерируется компилятором и, как было обещано, он содержит поля для состояния и текущего объекта, плюс по одному полю на каждую локальную переменную. Свойство Current просто возвращает текущий объект. Вся же настоящая работа происходит в методе MoveNext.

Для генерации метода MoveNext компилятор берёт написанный вами код и производит несколько трансформаций:
  • Все ссылки на переменные должны быть скорректированы, поскольку код перенёсся во вспомогательный класс.
  • this становится this$0, поскольку внутри сгенерированной функции this указывает на автоматически сгенерированный класс вместо оригинального.
  • m становится this$0.m, если m является членом исходного класса (нестатическим полем, свойством или методом). В действительности, это правило излишне в сочетании с предыдущим, т.к. запись имени члена класса без префикса m это просто сокращение для this.m.
  • v становится this.v, если v это параметр или локальная переменная. Это правило также излишне, т.к. запись v равносильна this.v, но я обращаюсь явно, чтобы вы обратили внимание, что хранилище переменной изменилось.

Помимо этого компилятору приходится иметь дело со всеми инструкциями yield return.
Каждое выраженик yield return x преобразуется в
current$0 = x;
state$0 = n;
return true;
resume$n:;
где n это возрастающее число, начинающееся с единицы 1.

Кроме этого есть выражения yield break.
Каждое выражение yield break преобразуется в
state$0 = n2;
return false;
где n2 — число, на единицу большее, чем наибольший номер из всех состояний, использующихся в выражениях yield return. Не забывайте, что в конце каждой функции подразумевается вызов yield break.

Наконец, компилятор вставляет большой диспетчер состояний в самом начале функции.
switch (state$0) {
    case 0: goto resume$0;
    case 1: goto resume$1;
    case 2: goto resume$2;
    // ...
    case n: goto resume$n;
    case n2: return false;
}
по одному case-выражению создаётся для каждого состояния, плюс на начальное и конечное состояние n2.
Tags:
Hubs:
Total votes 29: ↑24 and ↓5+19
Comments16

Articles