От переводчика:
Не так давно мой менее опытный коллега спросил меня о том, для чего используется yield return в C#. Я не очень часто пишу свои итераторы, поэтому, отвечая ему, я сомневался в своих словах. Справившись в MSDN, я укрепился в сказанном, но у меня возник вопрос: “А во что же всё таки компилируется эта инструкция?” К тому моменту, я уже был знаком с переводимой статьёй, однако всё, что в ней сказано, давно “выветрилось”. Статья старая, но мне думается, что она может быть полезна для определённой группы разработчиков, привыкшей читать русскоязычные статьи и документы.
Ссылка на продолжение: реализация итераторов в C# (часть 2)
Как и анонимные методы, итераторы в C# являются сложным синтаксическим сахаром. Вы можете реализовать их полностью самостоятельно (в конце концов, в ранних версиях C# вам приходилось делать это), но использовать компилятор намного удобнее. Идея, стоящая за итераторами заключается в том, что они принимают функцию с yield return выражениями (и, возможно, yield break выражениями) и конвертируют её в конечный автомат. Когда вызывается yield return, состояние функции сохраняется, и при повторном обращении к итератору для получения очередного объекта это состояние восстанавливается. Главное в итераторах то, что все локальные переменные итератора (в том числе параметры итератора как предварительно инициализировнные локальные переменные, включая скрытый параметр this) становятся переменными-членами (далее полями) вспомогательного класса. Помимо этого вспомогательный класс содержит поле state, которое следит, где произошло прерывание исполнения, и поле current, хранящее самый последний из уже перечисленных объектов.
Метод CountFrom создаёт перечислитель целых чисел, который производит целые числа от start до limit включительно с шагом 1. Компилятор неявно конвертирует этот перечислитель во что-то вроде этого:
Перечисляющий класс автоматически генерируется компилятором и, как было обещано, он содержит поля для состояния и текущего объекта, плюс по одному полю на каждую локальную переменную. Свойство Current просто возвращает текущий объект. Вся же настоящая работа происходит в методе MoveNext.
Для генерации метода MoveNext компилятор берёт написанный вами код и производит несколько трансформаций:
Помимо этого компилятору приходится иметь дело со всеми инструкциями yield return.
Каждое выраженик yield return x преобразуется в
Кроме этого есть выражения yield break.
Каждое выражение yield break преобразуется в
Наконец, компилятор вставляет большой диспетчер состояний в самом начале функции.
Не так давно мой менее опытный коллега спросил меня о том, для чего используется 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 преобразуется в
где n это возрастающее число, начинающееся с единицы 1.current$0 = x; state$0 = n; return true; resume$n:;
Кроме этого есть выражения yield break.
Каждое выражение yield break преобразуется в
где n2 — число, на единицу большее, чем наибольший номер из всех состояний, использующихся в выражениях yield return. Не забывайте, что в конце каждой функции подразумевается вызов yield break.state$0 = n2; return false;
Наконец, компилятор вставляет большой диспетчер состояний в самом начале функции.
по одному case-выражению создаётся для каждого состояния, плюс на начальное и конечное состояние n2.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; }
