От переводчика:
Не так давно мой менее опытный коллега спросил меня о том, для чего используется 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 преобразуется в
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.