Не часто приходится использовать ключевое слово yield. Обычно про него вспоминают при создании класса, реализующего интерфейс IEnumerable. Давайте разберемся, зачем оно нужно и как работает. А так же выясним, всегда ли надо разрабатывать класс для указанного выше интерфейса.
MSDN сухо поясняет: "ключевое слово yield сообщает компилятору, что метод, в котором оно содержится, является блоком итератора". Там же указано, что используется оно в сочетании с ключевыми словами return и break. Что это обозначает на практике? Давайте рассмотрим кратко и подробно.
Коротко о главном
Метод, использующий 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 и почему сохраняются переменные, то перейдем к деталям его реализации. Посмотрим что получилось в результате работы компилятора.
Небольшая подсказка: обратите внимание, что внутренние переменные метода сохраняют значения между его вызовами. Это должно натолкнуть на мысль, что они не могут быть в стеке. А значит должен появиться новый объект для, как минимум, хранения их в куче. Еще не догадались?
Загрузим полученный 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 самостоятельно.
На этом все. Остается только приложить исходный код (C#, Visual Studio 2010):
YieldKeywordDemo.zip (6.64 kb)