![]() Главная страница Случайная страница КАТЕГОРИИ: АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника |
Перечислители и итераторы
Рассмотрим стандартный код, работающий с массивом. Вначале массив инициализируется, затем печатаются все его элементы: 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, которая будет описана далее.
|