, обеспечивающий инфраструктуру, которая позволяет вызывающему коду обходить внутренние объекты, содержащиеся в совместимом с
IEnumerable
контейнере:
// Этот интерфейс позволяет вызывающему коду получать элементы контейнера.
public interface IEnumerator
{
bool MoveNext ; // Переместить вперед внутреннюю позицию курсора.
object Current { get;} //
Получить текущий элемент
// (свойство только для чтения).
void Reset ; // Сбросить курсор в позицию перед первым элементом.
}
Если вы хотите обновить тип
Garage
для поддержки этих интерфейсов, то можете пойти длинным путем и реализовать каждый метод вручную. Хотя вы определенно вольны предоставить специализированные версии методов
GetEnumerator
,
MoveNext
,
Current
и
Reset
, существует более легкий путь. Поскольку тип
System.Array
(а также многие другие классы коллекций) уже реализует интерфейсы
IEnumerable
и
IEnumerator
, вы можете просто делегировать запрос к
System.Array
следующим образом (обратите внимание, что в файл кода понадобится импортировать пространство имен
System.Collections
):
using System.Collections;
...
public class Garage : IEnumerable
{
// System.Array уже реализует IEnumerator!
private Car[] carArray = new Car[4];
public Garage
{
carArray[0] = new Car("FeeFee", 200);
carArray[1] = new Car("Clunker", 90);
carArray[2] = new Car("Zippy", 30);
carArray[3] = new Car("Fred", 30);
}
// Возвратить IEnumerator объекта массива.
public IEnumerator GetEnumerator
=> carArray.GetEnumerator;
}
После такого изменения тип
Garage
можно безопасно использовать внутри конструкции
foreach
. Более того, учитывая, что метод
GetEnumerator
был определен как открытый, пользователь объекта может также взаимодействовать с типом
IEnumerator
:
// Вручную работать с 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
{
...
// Итераторный метод.
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;
}
}
Если функция вызывается, как показано далее, и больше ничего не делается, тогда исключение никогда не сгенерируется: