Студопедия

Главная страница Случайная страница

Разделы сайта

АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника






Перечислители и итераторы






Рассмотрим стандартный код, работающий с массивом. Вначале массив инициализируется, затем печатаются все его элементы:

int[] data = {1, 2, 4, 8};

foreach (int item in data)

{

Console.WriteLine(item);

}

Данный код работоспособен по двум причинам. Во-первых, любой массив относится к перечисляемым типам. Во-вторых, оператор foreach знает, как действовать с объектами перечисляемых типов. Перечисляемый тип (enumerable type) – это тип с экземплярным методом GetEnumerator(), возвращающим перечислитель. Перечислитель (enumerator) – объект, обладающий свойством Current, представляющим текущий элемент набора, и методом MoveNext() для перемещения к следующему элементу. Оператор foreach получает перечислитель, вызывая метод GetEnumerator(), а затем использует MoveNext() и Current для итерации по набору.

// семантика работы оператора foreach из предыдущего примера

int[] data = {1, 2, 4, 8};

var enumerator = data.GetEnumerator();

int item;

while (enumerator.MoveNext())

{

item = (int) enumerator.Current;

Console.WriteLine(item);

}

В дальнейших примерах параграфа будет использоваться класс Shop, представляющий «магазин», который хранит некие «товары».

public class Shop

{

private string[] _items = new string[0];

 

public int ItemsCount

{

get { return _items.Length; }

}

 

public void AddItem(string item)

{

Array.Resize(ref _items, ItemsCount + 1);

_items[ItemsCount - 1] = item;

}

 

public string GetItem(int index)

{

return _items[index];

}

}

Пусть требуется сделать класс Shop перечисляемым. Для этого существует три способа:

1. Реализовать интерфейсы IEnumerable и IEnumerator.

2. Реализовать интерфейсы IEnumerable< T> и IEnumerator< T>.

3. Способ, при котором стандартные интерфейсы не применяются.

Интерфейсы IEnumerable и IEnumerator описаны в пространстве имён System.Collections:

public interface IEnumerable

{

IEnumerator GetEnumerator();

}

 

public interface IEnumerator

{

object Current { get; }

bool MoveNext();

void Reset();

}

Свойство для чтения Current представляет текущий объект набора. Для обеспечения универсальности это свойство имеет тип object. Метод MoveNext() выполняет перемещение на следующую позицию в наборе. Этот метод возвращает значение true, если дальнейшее перемещение возможно. Предполагается, что MoveNext() нужно вызвать и для получения первого элемента, то есть начальная позиция – «перед первым элементом». Метод Reset() сбрасывает позицию в начальное состояние.

Добавим поддержку интерфейсов IEnumerable и IEnumerator в класс Shop. Обратите внимание, что для этого используется вложенный класс, реализующий интерфейс IEnumerator.

public class Shop: IEnumerable

{

// опущены элементы ItemsCount, AddItem(), GetItem()

 

private class ShopEnumerator: IEnumerator

{

private readonly string[] _data; // локальная копия данных

private int _position = -1; // текущая позиция в наборе

 

public ShopEnumerator(string[] values)

{

_data = new string[values.Length];

Array.Copy(values, _data, values.Length);

}

 

public object Current

{

get { return _data[_position]; }

}

 

public bool MoveNext()

{

if (_position < _data.Length - 1)

{

_position++;

return true;

}

return false;

}

 

public void Reset()

{

_position = -1;

}

}

 

public IEnumerator GetEnumerator()

{

return new ShopEnumerator(_items);

}

}

Теперь класс Shop можно использовать следующим образом:

var shop = new Shop();

shop.AddItem(" computer");

shop.AddItem(" monitor");

foreach (string s in shop)

{

Console.WriteLine(s);

}

При записи цикла foreach объявляется переменная, тип которой совпадает с типом элемента коллекции. Так как свойство IEnumerator.Current имеет тип object, то на каждой итерации выполняется приведение этого свойства к типу переменной цикла[6]. Это может повлечь ошибки времени выполнения. Избежать ошибок помогает реализация перечисляемого типа при помощи универсальных интерфейсов IEnumerable< T> и IEnumerator< T>:

public interface IEnumerable< out T>: IEnumerable

{

IEnumerator< T> GetEnumerator();

}

 

public interface IEnumerator< out T>: IDisposable, IEnumerator

{

T Current { get; }

}

Универсальные интерфейсы IEnumerable< T> и IEnumerator< T> наследуются от обычных версий. У интерфейса IEnumerator< T> типизированное свойство Current. Тип, реализующий интерфейс IEnumerable< T>, должен содержать две версии метода GetEnumerator(). Обычно для IEnumerable.GetEnumerator() применяется явная реализация.

public class Shop: IEnumerable< string>

{

// опущены элементы ItemsCount, AddItem(), GetItem()

 

private class ShopEnumerator: IEnumerator< string>

{

private readonly string[] _data;

private int _position = -1;

 

public ShopEnumerator(string[] values)

{

_data = new string[values.Length];

Array.Copy(values, _data, values.Length);

}

 

public string Current

{

get { return _data[_position]; }

}

 

object IEnumerator.Current

{

get { return _data[_position]; }

}

 

public bool MoveNext()

{

if (_position < _data.Length - 1)

{

_position++;

return true;

}

return false;

}

 

public void Reset()

{

_position = -1;

}

 

public void Dispose() { /* пустая реализация */ }

}

 

public IEnumerator< string> GetEnumerator()

{

return new ShopEnumerator(_items);

}

 

IEnumerator IEnumerable.GetEnumerator()

{

return GetEnumerator();

}

}

Возможна (хотя и нетипична) реализация перечисляемого типа без использования стандартных интерфейсов:

public class Shop

{

// опущены элементы ItemsCount, AddItem(), GetItem()

 

public ShopEnumerator GetEnumerator()

{

return new ShopEnumerator(_items);

}

}

 

public class ShopEnumerator

{

// реализация соответствует первому примеру,

// где ShopEnumerator – вложенный класс

// метод Reset() не реализован (это не часть перечислителя)

public string Current { get {... } }

 

public bool MoveNext() {... }

}

Во всех предыдущих примерах для перечислителя создавался пользовательский класс. Существуют альтернативные подходы к реализации перечислителя. Так как перечисляемый тип обычно хранит свои данные в стандартной коллекции или массиве, то обычно достаточно вернуть перечислитель этой коллекции:

public class Shop: IEnumerable

{

// опущены элементы ItemsCount, AddItem(), GetItem()

 

public IEnumerator GetEnumerator()

{

return _items.GetEnumerator(); // перечислитель массива

}

}

Создать перечислить можно при помощи итератора. Итератор (iterator) – это операторный блок, который порождает упорядоченную последовательность значений. Итератор отличает присутствие одного или нескольких операторов yield. Оператор yield return выражение возвращает следующее значение последовательности, а оператор yield break прекращает генерацию последовательности. Итераторы могут использоваться в качестве тела метода, если тип метода – один из интерфейсов IEnumerator, IEnumerator< T>, IEnumerable, IEnumerable< T>.

Реализуем метод Shop.GetEnumerator() при помощи итератора.

public class Shop: IEnumerable< string>

{

// опущены элементы ItemsCount, AddItem(), GetItem()

 

public IEnumerator< string> GetEnumerator()

{

foreach (var s in _items)

{

yield return s;

}

}

}

Как видим, код заметно упростился. Элементы коллекции перебираются в цикле, и для каждого вызывается yield return s. Но достоинства итераторов этим не ограничиваются. В следующем примере в класс Shop добавляется метод, позволяющий перебрать коллекцию в обратном порядке.

 

public class Shop: IEnumerable< string>

{

// опущены элементы, описанные ранее

 

public IEnumerable< string> GetItemsReversed()

{

for (var i = _items.Length; i > 0; i--)

{

yield return _items[i - 1];

}

}

}

 

// пример использования

foreach (var s in shop.GetItemsReversed())

{

Console.WriteLine(s);

}

Итераторы реализуют концепцию отложенных вычислений. Каждое выполнение оператора yield return ведёт к выходу из метода и возврату значения. Но состояние метода, его внутренние переменные и позиция yield return запоминаются, чтобы быть восстановленными при следующем вызове.

Поясним концепцию отложенных вычислений на примере. Пусть имеется класс Helper с итератором GetNumbers().

public static class Helper

{

public static IEnumerable< int> GetNumbers()

{

int i = 0;

while (true) yield return i++;

}

}

Кажется, что вызов метода GetNumbers() приведёт к «зацикливанию» программы. Однако использование итераторов обеспечивает этому методу следующее поведение. При первом вызове GetNumbers() вернёт значение i = 0, и состояние метода (значение переменной i) будет зафиксировано. При следующем вызове метод вернёт значение i = 1 и снова зафиксирует своё состояние, и так далее. Таким образом, следующий код успешно печатает три числа:

foreach (var number in Helper.GetNumbers())

{

Console.WriteLine(number);

if (number == 2) break;

}

Рассмотрим ещё один пример итераторов. Пусть описан класс Range:

public class Range

{

public int Low { get; set; }

public int High { get; set; }

 

public IEnumerable< int> GetNumbers()

{

for (int counter = Low; counter < = High; counter++)

{

yield return counter;

}

}

}

Используем класс Range следующим образом:

var range = new Range {Low = 0, High = 10};

var enumerator = range.GetNumbers();

foreach (int number in enumerator)

{

Console.WriteLine(number);

}

На консоль будут выведены числа от 0 до 10. Интересно, что если изменить объект range после получения перечислителя enumerator, это повлияет на выполнение цикла foreach. Следующий код выводит числа от 0 до 5.

var range = new Range {Low = 0, High = 10};

var enumerator = range.GetNumbers();

range.High = 5; // изменяем свойство объекта range

foreach (int number in enumerator)

{

Console.WriteLine(number);

}

Возможности итераторов широко используются в технологии LINQ to Objects, которая будет описана далее.






© 2023 :: MyLektsii.ru :: Мои Лекции
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав.
Копирование текстов разрешено только с указанием индексируемой ссылки на источник.