// Вручную работать с 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 для возвращения своего содержимого. Итераторный метод допускается определять и так:
(window.adrunTag = window.adrunTag || []).push({v: 1, el: 'adrun-4-390', c: 4, b: 390})
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(), который рассматривается далее.