Visual Basic, .NET, ASP, VBScript
 

   
 
Описание для автора не найдено
 
     
   
 
22. Итераторы
Перевод: Шатохина Надежда(sna@uneta.org), Ukraine .Net Alliance (http://www.uneta.org)
Июль 2003
Применяется к:
  • Microsoft C#
  • Обзор:
    Спецификация C#. Глава 22: Итераторы. В этой главе детально описаны итераторы.
    Содерджание:
  • 22.1 Блоки итераторов
  • 22.1.1 Интерфейсы перечислителя
  • 22.1.2 Перечислимые интерфейсы
  • 22.1.3 Производимый тип
  • 22.1.4 This доступ
  • 22.2 Объекты-перечислители
  • 22.2.1 Метод MoveNext
  • 22.2.2 Свойство Current
  • 22.2.3 Метод Dispose
  • 22.3 Перечислимые объекты
  • 22.3.1 Метод GetEnumerator
  • 22.4 Оператор yield
  • 22.4.1 Определенное присваивание
  • 22.5 Пример реализации
  • 22.1 Блоки итераторов

    Блок итератора — это блок (block) (§8.2), который формирует упорядоченную последовательность значений. Блок итератора отличается от блока обычного оператора наличием одного или более операторов yield.

    • Оператор yield return генерирует следующее значение итерации.
    • Оператор yield break показывает, что итерация завершена.

    Блок итератора может использоваться как тело метода (method-body), тело оператора (operator-body) или тело аксессора (accessor-body), если возвращаемый тип члена соответствующей функции является одним из интерфейсов перечислителя (§22.1.1) или одним из перечислимых интерфейсов (§22.1.2).

    Блоки итераторов не являются отдельным элементом в грамматике C#. Они ограничены несколькими способами и основное влияние имеют на семантики описания членов функции, но грамматически они — просто блоки.

    Когда член функции реализовывается с использованием блока итератора, определение списком формальных параметров члена функции каких-либо параметров ref или out приводит к ошибке компилятора.

    При появлении оператора return в блоке итератора возникает ошибка компиляции (но оператор yield return разрешен).

    Если в блоке итератора имеется небезопасный контекст, возникает ошибка компиляции (§18.1). Блок итератора всегда определяет безопасный контекст, даже когда его описание вложено в небезопасный контекст.

    22.1.1 Интерфейсы перечислителя

    Интерфейсы перечислителя — это нешаблонный интерфейс System.Collections.IEnumerator и все экземпляры шаблонного интерфейса System.Collections.Generic.IEnumerator<T>. В этой главе эти интерфейсы упоминаются как IEnumerator и IEnumerator<T>, соответственно.

    22.1.2 Перечислимые интерфейсы

    Перечислимые интерфейсы — это нешаблонный интерфейс System.Collections.IEnumerable и все экземпляры шаблонного интерфейса System.Collections.Generic.IEnumerable<T>. В этой главе эти интерфейсы упоминаются как IEnumerable and IEnumerable<T>, соответственно.

    22.1.3 Производимый тип

    Блок итератора производит последовательность значений, все одного типа. Этот тип называют производимым типом блока итератора.

    • Производимый тип блока итератора используется для реализации члена функции, который возвращает объект реализующий IEnumerator или IEnumerable.
    • Производимый тип блока итератора используется для реализации члена функции, который возвращает составной объект IEnumerator<T> или IEnumerable<T>.

    22.1.4 This доступ

    В рамках блока итератора члена экземпляра класса выражение this классифицируется как значение. Тип-значение — это класс, в котором происходит использование, а значение — ссылка на объект, для которого был вызван член.

    В рамках блока итератора члена экземпляра структуры выражение this классифицируется как переменная. Тип переменной — это структура, в которой происходит использование. Переменная представляет копию (copy) структуры, для которой был вызван член. Переменная this в блоке итератора члена экземпляра структуры ведет себя точно так же, как и параметр-значение типа структуры.

    22.2 Объекты-перечислители

    Когда член функции, возвращающий тип интерфейса перечислителя, реализовывается с помощью блока итератора, код блока итератора при вызове члена функции выполняется не сразу. Вместо этого создается и возвращается объект-перечислитель. Этот объект инкапсулирует код, определенный в блоке итератора, и выполнение кода блока итератора происходит, когда вызывается метод MoveNext объекта-перечислителя. Объект-перечислитель имеет следующие характеристики:

    • Он реализовывает IEnumerator и IEnumerator<T>, где T — производимый тип блока итератора.
    • Он реализовывает System.IDisposable.
    • Он инициализируется копией значений аргумента (если таковые имеются) и значение экземпляра передается члену функции.
    • У него четыре потенциальных состояния: до, выполняется, приостановлен и после; исходным является состояние до.

    Обычно объект-перечислитель — это экземпляр сгенерированного компилятором класса-перечислителя, который инкапсулирует код в блоке итератора и реализовывает интерфейсы перечислителя, но возможны и другие методы реализации. Если класс-перечислитель генерируется компилятором, этот класс будет вложен, прямо или косвенно, в класс, содержащий член функции, он будет закрытым и будет иметь имя, зарезервированное для использования компилятором (§2.4.2).

    Объект-перечислитель может реализовывать большее количество интерфейсов, чем определено выше.

    Следующие разделы описывают точное поведение членов MoveNext, Current и Dispose реализаций интерфейсов IEnumerable и IEnumerable<T>, предоставленных объектом-перечислителем.

    Обратите внимание, что объекты-перечислители не поддерживают метод IEnumerator.Reset. При вызове этого метода возникнет шибка System.NotSupportedException.

    22.2.1 Метод MoveNext

    Метод MoveNext объекта-перечислителя инкапсулирует код блока итератора. Вызов метода MoveNext приводит к выполнению кода блока итератора и соответствующим образом задает свойство Current объекта-перечислителя. Предыдущее действие, осуществляемое MoveNext, зависит от состояния объекта-перечислителя в момент вызова MoveNext:

    • Если объект-перечислитель находится в состоянии до, вызов MoveNext:
      • Изменяет состояние на выполняется.
      • Инициализирует параметры (включая this) блока итератора с теми значениями аргументов и значениями экземпляров, которые были сохранены в момент инициализации объекта-перечислителя.
      • Выполняет блок итератора с начала до тех пор, пока выполнение не будет прервано (как описано ниже).
    • Если объект-перечислитель находится в состоянии выполняется, результат вызова MoveNext не определен.
    • Если объект-перечислитель находится в состоянии приостановлен, вызов MoveNext:
      • Изменяет состояние на выполняется.
      • Восстанавливает значения всех локальных переменных и параметров (включая this) в значения, сохраненные в момент, когда выполнение блока итератора было приостановлено. Обратите внимание, что содержимое любого объекта, используемого этими переменными, может измениться со времени предыдущего вызова MoveNext.
      • Возобновляет выполнение блока итератора сразу за оператором yield return, который обусловливает прерывание выполнения, и продолжает выполнение до тех пор, пока оно не будет прервано (как описано ниже).
    • Если объект-перечислитель находится в состоянии после, вызов MoveNext возвращает false.

    Выполнение блока итератора методом MoveNext может быть прервано четырьмя способами: оператором yield return, оператором yield break, при достижении конца блока итератора и при возникновении исключительной ситуации и распространении ее за пределы блока итератора.

    • Когда встречается оператор yield return (§22.4):
      • Выражение, заданное в операторе, вычисляется, неявно преобразовывается в производимый тип и присваивается свойству Current объекта-перечислителя.
      • Выполнение тела итератора приостанавливается. Значения всех локальных переменных и параметров (включая this) сохраняются, так же как и местоположение этого оператора yield return. Если оператор yield return располагается в пределах одного и более блоков try, ассоциированные блоки finally в этот момент не выполняются.
      • Состояние объекта-перечислителя меняется на приостановлен.
      • Метод MoveNext возвращает true объекту, вызвавшему его, показывая, что итерация успешно перешла к следующему значению.
    • Когда встречается оператор yield break (§22.4):
      • Если оператор yield break располагается в одном или более блоках try, выполняются ассоциированные блоки finally.
      • Состояние объекта-перечислителя изменяется на после.
      • Метод MoveNext возвращает false объекту, вызвавшему его, показывая, что итерация завершена.
    • При достижении конца тела итератора:
      • Состояние объекта-перечислителя изменяется на после.
      • Метод MoveNext возвращает false объекту, вызвавшему его, показывая, что итерация завершена.
    • При возникновении исключительной ситуации и распространении ее за пределы тела итератора:
      • При распространении действия исключительной ситуации будут выполнены соответствующие блоки finally в теле итератора.
      • Состояние объекта-перечислителя изменяется на после.
      • Действие исключительной ситуации распространится и на объект, вызывающий метод MoveNext.

    22.2.2 Свойство Current

    На свойство Current объекта-перечислителя оказывают влияние операторы yield return блока итератора.

    Когда объект-перечислитель находится в состоянии приостановлен, значением Current является значение, установленное последним вызовом MoveNext. Когда объект-перечислитель находится в состоянии до, выполняется или после, результат доступа к Current не определен.

    Для блока итератора с производимым типом, отличным от object, результат обращения к Current через реализацию IEnumerable объекта-перечислителя соответствует обращению к Current через реализацию IEnumerator<T> объекта-перечислителя и приведению результата к типу object.

    22.2.3 Метод Dispose

    Метод Dispose используется для очистки цикла путем переведения объекта-перечислителя в состояние после.

    • Если объект-перечислитель находится в состоянии до, вызов метода Dispose изменяет его состояние на после.
    • Если объект-перечислитель находится в состоянии выполняется, результат вызова метода Dispose не определен.
    • Если объект-перечислитель находится в состоянии приостановлен, вызов Dispose:
      • Изменяет состояние на выполняется.
      • Выполняет любые заключительные блоки так, как если бы последним выполненным оператором yield return был бы оператор yield break. Если это приводит к возникновению исключительной ситуации и распространению ее за пределы блока итератора, объект-перечислитель переходит в состояние после, и исключительная ситуация распространяется на объект, вызывающий метод Dispose.
      • Изменяет состояние на после.
    • Если объект-перечислитель находится в состоянии после, вызов Dispose не имеет никакого результата.

    22.3 Перечислимые объекты

    Когда член функции, возвращающий перечислимый тип интерфейса, реализован с использованием блока итератора, вызов члена функции не сразу выполняет код блока итератора. Вместо этого, создается и возвращается перечислимый объект. Метод GetEnumerator перечислимого объекта возвращает перечислимый объект, который инкапсулирует код, определенный в блоке итератора, выполнение кода блока итератора происходит при вызове метода MoveNext объекта-перечислителя. Перечислимый объект имеет следующие характеристики:

    • Он реализовывает IEnumerable и IEnumerable<T>, где T — производимый тип блока итератора.
    • Он инициализируется копией значений аргументов (если таковые имеются) и значением экземпляра, переданным члену функции.

    Перечислимый объект — это обычно экземпляр сгенерированного компилятором перечислимого класса, который инкапсулирует код в блок итератора и реализовывает перечислимые интерфейсы, но возможны и другие методы реализации. Если перечислимый класс сгенерирован компилятором, этот класс будет вложен, прямо или косвенно, в класс, содержащий член функции, он будет закрытым и будет иметь имя, зарезервированное для использования компилятором (§2.4.2).

    Перечислимый объект может реализовывать большее количество интерфейсов, чем показано выше. В частности, перечислимый объект может также реализовывать IEnumerator и IEnumerator<T>, что дает ему возможность выступать как в роли перечислимого, так и в роли перечислителя. В реализации такого типа при первом вызове метода GetEnumerator перечислимого объекта возвращается сам перечислимый объект. Последующие вызовы GetEnumerator перечислимого объекта, если таковые будут, возвращают копию перечислимого объекта. Таким образом, каждый возвращенный перечислитель имеет собственное состояние и изменения в одном перечислителе не оказывают влияния на другие.

    22.3.1 Метод GetEnumerator

    Перечислимый объект предоставляет реализацию методов GetEnumerator интерфейсов IEnumerable и IEnumerable<T>. Два метода GetEnumerator совместно используют общую реализацию, которая запрашивает и возвращает доступный объект-перечислитель. Объект-перечислитель инициализируется значениями аргументов и значением экземпляра, сохраненными при инициализации перечислимого объекта, но, в противном случае, объект-перечислитель функционирует, как описано в §22.2.

    22.4 Оператор yield

    Оператор yield используется в блоке итератора для формирования значений в объект-перечислитель или для предупреждения об окончании итерации.

    embedded-statement:
    	...
    	yield-statement
    yield-statement:
    	yield   return   expression   ;
    	yield   break   ;
    

    Чтобы обеспечить совместимость с существующими программами, помните, yield — не зарезервированное слово и имеет специальное значение только, когда используется сразу перед ключевыми словами return или break. В других контекстах yield может использоваться как идентификатор.

    Существует несколько ограничений на то, где может появиться оператор yield:

    • При появлении оператора (или формы) yield вне тела метода (method-body), тела оператора (operator-body) или тела аксессора (accessor-body), возникает ошибка компилятора.
    • При появлении оператора (или формы) yield в анонимном методе, возникает ошибка компилятора.
    • При появлении оператора (или формы) yield в операторе finally или выражении try, возникает ошибка компилятора.
    • При появлении оператора yield return где-либо в выражении try, которое содержит операторы catch, возникает ошибка компилятора.

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

    delegate IEnumerable<int> D();
    IEnumerator<int> GetEnumerator() {
    	try {
    		yield return 1;	// Ok
    		yield break;		// Ok
    	}
    	finally {
    		yield return 2;	// Ошибка, yield в finally
    		yield break;		// Ошибка, yield в finally
    	}
    	try {
    		yield return 3;	// Ошибка, yield return в try...catch
    		yield break;		// Ok
    	}
    	catch {
    		yield return 4;	// Ошибка, yield return в try...catch
    		yield break;		// Ok
    	}
    	D d = delegate { 
    		yield return 5; 	// Ошибка, yield в анонимном методе 
    	}; 
    }
    int MyMethod() {
    	yield return 1;		// Ошибка, неверный возвращаемый тип блока итератора 
    }
    

    Должно существовать неявное преобразование (§6.1) из типа выражения в операторе yield return в производимый тип (§22.1.3) блока итератора.

    Оператор yield return выполняется следующим образом:

    • Заданное в операторе выражение обрабатывается, неявно преобразовывается в производимый тип и присваивается свойству Current объекта-перечислителя.
    • Выполнение блока итератора приостанавливается. Если оператор yield return находится в пределах одного или более блоков try, ассоциированные блоки finally в это время не выполняются.
    • Метод MoveNext объекта-перечислителя возвращает true вызывающему его объекту, показывая, что объект-перечислитель успешно перешел к следующему элементу.

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

    Оператор yield break выполняется следующим образом:

    • Если оператор yield break включен в один или более блоков try с ассоциированными блоками finally, управление в начальной стадии передается блоку finally самого внутреннего оператора try. Когда и если управление достигает конечной точки блока finally, управление передается блоку finally следующего внешнего оператора try. Этот процесс повторяется до тех пор, пока не будут выполнены блоки finally всех вложенных операторов try.
    • Управление возвращается вызывающему методу блока итератора, а это или метод MoveNext, или метод Dispose объекта-перечислителя.

    Поскольку оператор yield break безоговорочно передает управление куда угодно, конечной точки оператора yield break достигнуть никогда невозможно.

    22.4.1 Определенное присваивание

    Для оператора yield returnstmt в форме:

    yield return expr ;
    

    • Переменная v находится в таком же явно заданном состоянии в начале expr, как и в начале stmt.
    • Если переменная v явно задана в конце expr, она точно задана в конечной точке stmt; в противном случае, она не задана явно в конечной точке stmt.

    22.5 Пример реализации

    В этом разделе приведен пример возможной реализации итераторов в терминах стандартных структурных компонентов C#. Описанная здесь реализация основана на тех же принципах, что используются компилятором Microsoft C#, но это ни в коем случае не навязываемая и не единственно возможная реализация

    Следующий класс Stack<T> реализовывает свой метод GetEnumerator, используя итератор. Итератор перечисляет элементы Stack<T> сверху вниз.

    using System;
    using System.Collections;
    using System.Collections.Generic;
    class Stack<T>: IEnumerable<T>
    {
    	T[] items;
    	int count;
    	public void Push(T item) {
    		if (items == null) {
    			items = new T[4];
    		}
    		else if (items.Length == count) {
    			T[] newItems = new T[count * 2];
    			Array.Copy(items, 0, newItems, 0, count);
    			items = newItems;
    		}
    		items[count++] = item;
    	}
    	public T Pop() {
    		T result = items[--count];
    		items[count] = T.default;
    		return result;
    	}
    	public IEnumerator<T> GetEnumerator() {
    		for (int i = count - 1; i >= 0; --i) yield items[i];
    	}
    }
    

    Метод GetEnumerator может быть преобразован в экземпляр сгенерированного компилятором класса-перечислителя, который инкапсулирует код в блок итератора, как показано далее.

    class Stack<T>: IEnumerable<T>
    {
    	...
    	public IEnumerator<T> GetEnumerator() {
    		return new __Enumerator1(this);
    	}
    	class __Enumerator1: IEnumerator<T>, IEnumerator
    	{
    		int __state;
    		T __current;
    		Stack<T> __this;
    		int i;
    		public __Enumerator1(Stack<T> __this) {
    			this.__this = __this;
    		}
    		public T Current {
    			get { return __current; }
    		}
    		object IEnumerator.Current {
    			get { return __current; }
    		}
    		public bool MoveNext() {
    			switch (__state) {
    				case 1: goto __state1;
    				case 2: goto __state2;
    			}
    			i = __this.count - 1;
    		__loop:
    			if (i < 0) goto __state2;
    			__current = __this.items[i];
    			__state = 1;
    			return true;
    		__state1:
    			--i;
    			goto __loop;
    		__state2:
    			__state = 2;
    			return false;
    		}
    		public void Dispose() {
    			__state = 2;
    		}
    		void IEnumerator.Reset() {
    			throw new NotSupportedException();
    		}
    	}
    }
    

    В предыдущем преобразовании код в блоке итератора превращается в конечный автомат и помещается в метод MoveNext класса-перечислителя. Более того, локальная переменная i превращается в поле объекта-перечислителя, таким образом, она может продолжать существовать между вызовами MoveNext.

    Следующий пример формирует простую таблицу умножения целых от 1 до 10. Метод FromTo в примере возвращает перечислимый объект и реализован с использованием итератора.

    using System;
    using System.Collections.Generic;
    class Test
    {
    	static IEnumerable<int> FromTo(int from, int to) {
    		while (from <= to) yield return from++;
    	}
    	static void Main() {
    		IEnumerable<int> e = FromTo(1, 10);
    		foreach (int x in e) {
    			foreach (int y in e) {
    				Console.Write("{0,3} ", x * y);
    			}
    			Console.WriteLine();
    		}
    	}
    }
    

    Метод FromTo может быть преобразован в экземпляр сгенерированного компьютером перечислимого класса, который инкапсулирует код в блоке итератора, как показано далее.

    using System;
    using System.Threading;
    using System.Collections;
    using System.Collections.Generic;
    class Test
    {
    	...
    	static IEnumerable<int> FromTo(int from, int to) {
    		return new __Enumerable1(from, to);
    	}
    	class __Enumerable1:
    		IEnumerable<int>, IEnumerable,
    		IEnumerator<int>, IEnumerator
    	{
    		int __state;
    		int __current;
    		int __from;
    		int from;
    		int to;
    		int i;
    		public __Enumerable1(int __from, int to) {
    			this.__from = __from;
    			this.to = to;
    		}
    		public IEnumerator<int> GetEnumerator() {
    			__Enumerable1 result = this;
    			if (Interlocked.CompareExchange(ref __state, 1, 0) != 0) {
    				result = new __Enumerable1(__from, to);
    				result.__state = 1;
    			}
    			result.from = result.__from;
    			return result;
    		}
    		IEnumerator IEnumerable.GetEnumerator() {
    			return (IEnumerator)GetEnumerator();
    		}
    		public int Current {
    			get { return __current; }
    		}
    		object IEnumerator.Current {
    			get { return __current; }
    		}
    		public bool MoveNext() {
    			switch (__state) {
    			case 1:
    				if (from > to) goto case 2;
    				__current = from++;
    				__state = 1;
    				return true;
    			case 2:
    				__state = 2;
    				return false;
    			default:
    				throw new InvalidOperationException();
    			}
    		}
    		public void Dispose() {
    			__state = 2;
    		}
    		void IEnumerator.Reset() {
    			throw new NotSupportedException();
    		}
    	}
    }
    

    Перечислимый класс реализовывает и перечислимые интерфейсы, и интерфейсы перчислителя, что обеспечивает ему возможность работать и как перечислимый, и как перечислитель. При первом вызове метода GetEnumerator возвращается сам перечислимый объект. Последующие вызовы GetEnumerator перечислимого объекта, если таковые последуют, возвращают копию перечислимого объекта. Таким образом, у каждого возвращенного перечислителя есть собственное состояние, и изменения в одном перечислителе не отразятся на других. Метод Interlocked.CompareExchange используется для обеспечения работы в многопоточной среде.

    В перечислимом классе параметры from и to превращаются в поля. Поскольку from модифицируется в блоке итератора, вводится дополнительное поле __from для сохранения исходного значения, присвоенного from в каждом перечислителе.

    При вызове метода MoveNext, когда __state равно 0, возникает исключительная ситуация InvalidOperationException. Это предохраняет от использования перечислимого объекта в качестве объекта-перечислителя без предварительного вызова GetEnumerator.

    Никакая часть настоящей статьи не может быть воспроизведена или передана в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, если на то нет письменного разрешения владельцев авторских прав.

    Материал, изложенный в данной статье, многократно проверен. Но, поскольку вероятность технических ошибок все равно существует, сообщество не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим сообщество не несет ответственности за возможные ошибки, связанные с использованием статьи.
     
         

       
       
         
      VBNet рекомендует