Перебор объектов (интерфейс (Enumerable) и итераторы

Оператор foreach является удобным средством перебора элементов объекта. Массивы и все стандартные коллекции библиотеки.NET позволяют выполнять такой перебор благодаря тому, что в них реализованы интерфейсы IEnumerable и IEnumerator. Для применения оператора foreach к пользовательскому типу дан­ных требуется реализовать в нем эти интерфейсы. Давайте посмотрим, как это делается.

Интерфейс IEnumerable {перечислимып) определяет всего один метод — GetEnumerator, возвращающий объект типа IEnumerator (перечислитель), который можно исполь­зовать для просмотра элементов объекта.

Интерфейс IEnumerator задает три элемента:

□ свойство Current, возвращающее текущий элемент объекта;

□ метод MoveNext, продвигающий перечислитель на следующий элемент объекта;

□ метод Reset, устанавливающий перечислитель в начало просмотра.

Цикл foreach использует эти методы для перебора элементов, из которых состо­ит объект.

Таким образом, если требуется, чтобы для перебора элементов класса мог приме­няться цикл foreach, необходимо реализовать четыре метода: GetEnumerator, Current, MoveNext и Reset. Например, если внутренние элементы класса организованы в мас­сив, потребуется описать закрытое поле класса, хранящее текущий индекс в мас­сиве, в методе MoveNext задать изменение этого индекса на 1 с проверкой выхода за границу массива, в методе Current — возврат элемента массива по текущему индексу и т. д.

Это не интересная работа, а выполнять ее приходится часто, поэтому в версию 2.0 были введены средства, облегчающие выполнение перебора в объекте — итераторы. Итератор представляет собой блок кода, задающий последовательность перебо­ра элементов объекта. На каждом проходе цикла foreach выполняется один шаг

итератора, заканчивающийся выдачей очередного значения. Выдача значения выполняется с помощью ключевого слова yield.

Рассмотрим создание итератора на примере (листинг 9.5). Пусть требуется соз­дать объект, содержащий боевую группу экземпляров типа Monster, неоднократно использованного в примерах этой книги. Для простоты ограничим максималь­ное количество бойцов в группе десятью.

Листинг 9.5. Класс с итератором

using System;

using System.Collections;

namespace ConsoleApplication1

{

class Monster

{ //... }

class Daemon

{ //... }

class Stado: IEnumerable // 1

{

private Monster[] mas;

private int n;

public Stado()

{

mas = new Monster[10];

n = 0;

}

public IEnumerator GetEnumerator()

{

for (int i = 0; i < n; ++i) yield return mas[i]; // 2

}

public void Add(Monster m)

{

if (n >= 10) return;

mas[n] = m; ++n;

}

}

class Class1

{

static void Main()

{

Stado s = new Stado();

s.Add(new Monster());

s.Add(new Monster("Вася"));

s.Add(new Daemon());

foreach (Monster m in s) m.Passport();

}

}

}

}

}

Все, что требуется сделать в версии 2.0 для поддержки перебора, — указать, что класс реализует интерфейс IEnumerable (оператор 1), и описать итератор (опера­тор 2). Доступ к нему может быть осуществлен через методы MoveNext и Current интерфейса IEnumerator.

За кодом, приведенным в листинге 9.5, стоит большая внутренняя работа компи­лятора. На каждом шаге цикла foreach для итератора создается «оболочка» -служебный объект, который запоминает текущее состояние итератора и выпол­няет все необходимое для доступа к просматриваемым элементам объекта. Ины­ми словами, код, составляющий итератор, не выполняется так, как он выглядит -в виде непрерывной последовательности, а разбит на отдельные итерации, между которыми состояние итератора сохраняется.

В листинге 9.6 приведен пример итератора, перебирающего четыре заданных строки.

Листинг 9.6. Простейший итератор

using System;

using System.Collections;

namespace ConsoleApplication1

{

class Num: IEnumerable

{

public IEnumerator GetEnumerator()

{

yield return "one";

yield return "two";

yield return "three";

yield return "oops";

}

}

class Class1

{

static void Main()

{

foreach (string s in new Num()) Console.WriteLine(s);

}

}

}

Результат работы программы:

one

two

three

oops

Следующий пример демонстрирует перебор значений в заданном диапазоне (от 1 до 5):

using System;

using System.Collections;

namespace ConsoleApplication1

{

class Class1

{

public static IEnumerable Count(int from, int to)

{

from = 1;

while (from <= to) yield return from++;

}

static void Main()

{

foreach (int i in Count(1, 5)) Console.WriteLine(i);

}

}

}

Преимущество использования итераторов заключается в том, что для одного и того же класса можно задать различный порядок перебора элементов. В листинге 9.7 описаны две дополнительные стратегии перебора элементов класса Stado, введен­ного в листинге 9.5, — перебор в обратном порядке и выборка только тех объек­тов, которые являются экземплярами класса Monster (для этого использован ме­тод получения типа объекта GetType, унаследованный от базового класса object).

Листинг 9.7. Реализация нескольких стратегий перебора

using System;

using System.Collections;

using MonsterLib;

namespace ConsoleApplication1

{

class Monster

{ //... }

class Daemon

{ //... }

class Stado: IEnumerable

{

private Monster[] mas;

private int n;

public Stado()

{

mas = new Monster[10];

n = 0;

}

public IEnumerator GetEnumerator()

{

for (int i = 0; i < n; ++i) yield return mas[i];

}

public IEnumerable Backwards() // в обратном порядке

{

for (int i = n - 1; i >= 0; --i) yield return mas[i];

}

public IEnumerable MonstersOnly() // только монстры

{

for (int i = 0; i < n; ++i)

if (mas[i].GetType().Name == "Monster")

yield return mas[i];

}

public void Add(Monster m)

{

if (n >= 10) return; mas[n] = m; ++n;

}

}

class Class1

{

static void Main()

{

Stado s = new Stado();

s.Add(new Monster());

s.Add(new Monster("Вася"));

s.Add(new Daemon());

foreach (Monster i in s) i.Passport();

foreach (Monster i in s.Backwards()) i.Passport();

foreach (Monster i in s.MonstersOnly()) i.Passport();

}

}

}

}

}

Теперь, когда вы получили представление об итераторах, рассмотрим их более формально.

Блок итератора синтаксически представляет собой обычный блок и может встре­чаться в теле метода, операции или части get свойства, если соответствующее возвращаемое значение имеет тип IEnumerable или IEnumerator1. В теле блока итератора могут встречаться две конструкции:

□ yield return формирует значение, выдаваемое на очередной итерации;

□ yield break сигнализирует о завершении итерации.

Ключевое слово yield имеет специальное значение для компилятора только в этих конструкциях.

Код блока итератора выполняется не так, как обычные блоки. Компилятор фор­мирует служебный объект-перечислитель, при вызове метода MoveNext которого выполняется код блока итератора, выдающий очередное значение с помощью ключевого слова yield. Следующий вызов метода MoveNext объекта-перечислите­ля возобновляет выполнение блока итератора с момента, на котором он был при­остановлен в предыдущий раз.

1А также тип их параметризованных двойников IEnumerable <Т> или IEnumerator<T> из про­странства имен System. Col lections. Generic, описанного в главе 13.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: