// Вручную работать с IEnumerator.
IEnumerator carEnumerator = carLot.GetEnumerator();
carEnumerator.MoveNext();
Car myCar = (Car)i.Current;
Console.WriteLine("{0} is going {1} MPH", myCar.PetName, myCar.CurrentSpeed);
Тем не менее, если вы предпочитаете скрыть функциональность
IEnumerable
на уровне объектов, то просто задействуйте явную реализацию интерфейса:
// Возвратить IEnumerator объекта массива.
IEnumerator IEnumerable.GetEnumerator()
=> return carArray.GetEnumerator();
В результате обычный пользователь объекта не обнаружит метод
GetEnumerator()
в классе
Garage
, в то время как конструкция
foreach
при необходимости будет получать интерфейс в фоновом режиме.
Построение итераторных методов с использованием ключевого слова yield
Существует альтернативный способ построения типов, которые работают с циклом
foreach
, предусматривающий использование
итераторов. Попросту говоря,
итератор — это член, который указывает, каким образом должны возвращаться внутренние элементы контейнера во время обработки в цикле
foreach
. В целях иллюстрации создайте новый проект консольного приложения по имени
CustomEnumeratorWithYield
и вставьте в него типы
Car
,
Radio
и
Garage
из предыдущего примера (снова переименовав пространство имен согласно текущему проекту). Затем модифицируйте тип
Garage
:
public class Garage : IEnumerable
{
...
<b> // Итераторный метод.</b>
public IEnumerator GetEnumerator()
{
foreach (Car c in carArray)
{
yield return c;
}
}
}
Обратите внимание, что показанная реализация метода
GetEnumerator()
осуществляет проход по элементам с применением внутренней логики
foreach
и возвращает каждый объект
Car
вызывающему коду, используя синтаксис
yield return
. Ключевое слово
yield
применяется для указания значения или значений, которые подлежат возвращению конструкцией
foreach
вызывающему коду. При достижении оператора
yield return
текущее местоположение в контейнере сохраняется и выполнение возобновляется с этого местоположения, когда итератор вызывается в следующий раз.
Итераторные методы не обязаны использовать ключевое слово
foreach
для возвращения своего содержимого. Итераторный метод допускается определять и так:
public IEnumerator GetEnumerator()
{
yield return carArray[0];
yield return carArray[1];
yield return carArray[2];
yield return carArray[3];
}
В этой реализации обратите внимание на то, что при каждом своем прохождении метод
GetEnumerator()
явно возвращает вызывающему коду новое значение. В рассматриваемом примере поступать подобным образом мало смысла, потому что если вы добавите дополнительные объекты к переменной-члену
carArray
, то метод
GetEnumerator()
станет рассогласованным. Тем не менее, такой синтаксис может быть полезен, когда вы хотите возвращать из метода локальные данные для обработки посредством
foreach
.
Защитные конструкции с использованием локальных функций (нововведение в версии 7.0)
До первого прохода по элементам (или доступа к любому элементу) никакой код в методе
GetEnumerator()
не выполняется. Таким образом, если до выполнения оператора
yield
возникает условие для исключения, то оно не будет сгенерировано при первом вызове метода, а лишь во время первого вызова
MoveNext()
.
Чтобы проверить это, модифицируйте
GetEnumerator()
:
public IEnumerator GetEnumerator()
{
// Исключение не сгенерируется до тех пор, пока не будет вызван
// метод MoveNext().
throw new Exception("This won't get called");
foreach (Car c in carArray)
{
yield return c;
}
}
Если функция вызывается, как показано далее, и больше ничего не делается, тогда исключение никогда не сгенерируется:
using System.Collections;
...
Console.WriteLine("***** Fun with the Yield Keyword *****\n");
Garage carLot = new Garage();
IEnumerator carEnumerator = carLot.GetEnumerator();
Console.ReadLine();
Код выполнится только после вызова
MoveNext()
и сгенерируется исключение. В зависимости от нужд программы это может быть как вполне нормально, так и нет. Ваш метод
GetEnumerator()
может иметь
защитную конструкцию, которую необходимо выполнить при вызове метода в первый раз. В качестве примера предположим, что список формируется из базы данных. Вам может понадобиться организовать проверку, открыто ли подключение к базе данных, во время вызова метода, а не при проходе по списку. Или же может возникнуть потребность в проверке достоверности входных параметров метода
Iterator()
, который рассматривается далее.