Выбрать главу

Получить перечислитель, устанавливаемый в начало коллекции, вызвав для этой коллекции метод GetEnumerator().

Организовать цикл, в котором вызывается метод MoveNext(). Повторять цикл до тех пор, пока метод MoveNext() возвращает логическое значение true.

Получить в цикле каждый элемент коллекции с помощью свойства Current. Ниже приведен пример программы, в которой реализуется данная процедура. В этой программе используется класс ArrayList, но общие принципы циклического обращения к элементам коллекции с помощью перечислителя остаются неизменны ми для коллекций любого типа, в том числе и обобщенных. // Продемонстрировать применение перечислителя. using System; using System.Collections; class EnumeratorDemo { static void Main() { ArrayList list = new ArrayList(1); for (int i=0; i < 10; i++) list.Add(i); // Использовать перечислитель для доступа к списку. IEnumerator etr = list.GetEnumerator(); while(etr.MoveNext()) Console.Write(etr.Current + " "); Console.WriteLine(); // Повторить перечисление списка. etr.Reset(); while(etr.MoveNext()) Console.Write(etr.Current + " "); Console.WriteLine(); } } Вот к какому результату приводит выполнение этой программы. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 Вообще говоря, для циклического обращения к элементам коллекции цикл foreach оказывается более удобным, чем перечислитель. Тем не менее перечисли тель предоставляет больше возможностей для управления, поскольку его можно при желании всегда установить в исходное положение. Применение перечислителя типа IDictionaryEnumerator Если для организации коллекции в виде словаря, например типа Hashtable, реализуется необобщенный интерфейс IDictionary, то для циклического обра щения к элементам такой коллекции следует использовать перечислитель типа IDictionaryEnumerator вместо перечислителя типа IEnumerator. Интерфейс IDictionaryEnumerator наследует от интерфейса IEnumerator и имеет три допол нительных свойства. Первым из них является следующее свойство. DictionaryEntry Entry { get; } Свойство Entry позволяет получить пару "ключ-значение" из перечислителя в форме структуры DictionaryEntry. Напомним, что в структуре DictionaryEntry определяются два свойства, Key и Value, с помощью которых можно получать доступ к ключу или значению, связанному с элементом коллекции. Ниже приведены два дру гих свойства, определяемых в интерфейсе IDictionaryEnumerator. object Key { get; } object Value { get; } С помощью этих свойств осуществляется непосредственный доступ к ключу или значению. Перечислитель типа IDictionaryEnumerator используется аналогично обычному перечислителю, за исключением того, что текущее значение в данном случае получа ется с помощью свойств Entry, Key или Value, а не свойства Current. Следовательно, приобретя перечислитель типа IDictionaryEnumerator, необходимо вызвать метод MoveNext(), чтобы получить первый элемент коллекции. А для получения остальных ее элементов следует продолжить вызовы метода MoveNext(). Этот метод возвращает логическое значение false, когда в коллекции больше нет ни одного элемента. В приведенном ниже примере программы элементы коллекции типа Hashtable перечисляются с помощью перечислителя типа IDictionaryEnumerator. // Продемонстрировать применение перечислителя типа IDictionaryEnumerator. using System; using System.Collections; class IDicEnumDemo { static void Main() { // Создать хеш-таблицу. Hashtable ht = new Hashtable(); // Добавить элементы в таблицу. ht.Add("Кен", "555-7756"); ht.Add("Мэри", "555-9876"); ht.Add("Том", "555-3456"); ht.Add("Тодд", "555-3452"); // Продемонстрировать применение перечислителя. IDictionaryEnumerator etr = ht.GetEnumerator(); Console.WriteLine("Отобразить информацию с помощью свойства Entry."); while(etr.MoveNext()) Console.WriteLine(etr.Entry.Key + ": " + etr.Entry.Value); Console.WriteLine (); Console.WriteLine("Отобразить информацию " + "с помощью свойств Key и Value."); etr.Reset(); while(etr.MoveNext()) Console.WriteLine(etr.Key + ": " + etr.Value); } } Ниже приведен результат выполнения этой программы. Отобразить информацию с помощью свойства Entry. Мэри: 555-9876 Том: 555-3456 Тодд: 555-3452 Кен: 555-7756 Отобразить информацию с помощью свойств Key и Value. Мэри: 555-9876 Том: 555-3456 Тодд: 555-3452 Кен: 555-7756 Реализация интерфейсов IEnumerable и IEnumerator Как упоминалось выше, для циклического обращения к элементам коллекции за частую проще (да и лучше) организовать цикл foreach, чем пользоваться непосред ственно методами интерфейса IEnumerator. Тем не менее ясное представление о принципе действия подобных интерфейсов важно иметь по еще одной причине: если требуется создать класс, содержащий объекты, перечисляемые в цикле foreach, то в этом классе следует реализовать интерфейсы IEnumerator и IEnumerable. Иными словами, для того чтобы обратиться к объекту определяемого пользователем класса в цикле foreach, необходимо реализовать интерфейсы IEnumerator и IEnumerable в их обобщенной или необобщенной форме. Правда, сделать это будет нетрудно, по скольку оба интерфейса не очень велики. В приведенном ниже примере программы интерфейсы IEnumerator и IEnumerable реализуются в необобщенной форме, с тем чтобы перечислить содер жимое массива, инкапсулированного в классе MyClass. // Реализовать интерфейсы IEnumerable и IEnumerator. using System; using System.Collections; class MyClass : IEnumerator, IEnumerable { char[] chrs = { 'А', 'В', 'C', 'D' }; int idx = -1; // Реализовать интерфейс IEnumerable. public IEnumerator GetEnumerator() { return this; } // В следующих методах реализуется интерфейс IEnumerator // Возвратить текущий объект. public object Current { get { return chrs[idx]; } } // Перейти к следующему объекту. public bool MoveNext() { if(idx == chrs.Length-1) { Reset(); // установить перечислитель в конец return false; } idx++; return true; } // Установить перечислитель в начало. public void Reset() { idx = -1; } } class EnumeratorImplDemo { static void Main() { MyClass me = new MyClass(); // Отобразить содержимое объекта me. foreach(char ch in me) Console.Write(ch + " "); Console.WriteLine(); // Вновь отобразить содержимое объекта me. foreach(char ch in me) Console.Write(ch + " "); Console.WriteLine(); } } Эта программа дает следующий результат. А В С D А В С D В данной программе сначала создается класс MyClass, в котором инкапсулируется небольшой массив типа char, состоящий из символов А-D. Индекс этого массива хра нится в переменной idx, инициализируемой значением -1. Затем в классе MyClass ре ализуются оба интерфейса, IEnumerator и IEnumerable. Метод GetEnumerator() возвращает ссылку на перечислитель, которым в данном случае оказывается текущий объект. Свойство Current возвращает следующий символ в массиве, т.е. объект, ука зываемый по индексу idx. Метод MoveNext() перемещает индекс idx в следующее положение. Этот метод возвращает логическое значение false, если достигнут конец коллекции, в противном случае — логическое значение true. Напомним, что перечис литель оказывается неопределенным вплоть до первого вызова метода MoveNext(). Следовательно, метод MoveNext() автоматически вызывается в цикле foreach перед обращением к свойству Current. Именно поэтому первоначальное значение пере менной idx устанавливается равным -1. Оно становится равным нулю на первом шаге цикла foreach. Обобщенная реализация рассматриваемых здесь интерфейсов будет действовать по тому же самому принципу. Далее в методе Main() создается объект mc типа MyClass, и содержимое этого объекта дважды отображается в цикле foreach. Применение итераторов Как следует из предыдущих примеров, реализовать интерфейсы IEnumerator и IEnumerable нетрудно. Но еще проще воспользоваться итератором, который пред ставляет собой метод, оператор или аксессор, возвращающий по очереди члены со вокупности объектов от ее начала и до конца. Так, если некоторый массив состоит из пяти элементов, то итератор данного массива возвратит все эти элементы по очереди. Реализовав итератор, можно обращаться к объектам определяемого пользователем класса в цикле foreach. Обратимся сначала к простому примеру итератора. Приведенная ниже программа является измененной версией предыдущей программы, в которой вместо явной реали зации интерфейсов IEnumerator и IEnumerable применяется итератор. // Простой пример применения итератора. using System; using System.Collections; class MyClass { char[] chrs = { 'A', 'B', 'C', 'D' }; // Этот итератор возвращает символы из массива chrs. public IEnumerator GetEnumerator() { foreach(char ch in chrs) yield return ch; } } class ItrDemo { static void Main() { MyClass me = new MyClass(); foreach(char ch in me) Console.Write(ch + " "); Console.WriteLine(); } } При выполнении этой программы получается следующий результат. А В С D Как видите, содержимое массива mc.chrs перечислено. Рассмотрим эту программу более подробно. Во-первых, обратите внимание на то, что в классе MyClass не указывается IEnumerator в качестве реализуемого интерфей са. При создании итератора компилятор реализует этот интерфейс автоматически. И во-вторых, обратите особое внимание на метод GetEnumerator(), который ради удобства приводится ниже еще раз. // Этот итератор возвращает символы из массива chrs. public IEnumerator GetEnumerator() { foreach(char ch in chrs) yield return ch; } Это и есть итератор для объектов класса MyClass. Как видите, в нем явно реализу ется метод GetEnumerator(), определенный в интерфейсе IEnumerable. А теперь перейдем непосредственно к телу данного метода. Оно состоит из цикла foreach, в котором возвращаются элементы из массива chrs. И делается это с помощью опе ратора yield return. Этот оператор возвращает следующий объект в коллекции, которым в данном случае оказывается очередной символ в массиве chrs. Благодаря этому средству обращение к объекту mc типа MyClass организуется в цикле foreach внутри метода Main(). Обозначение yield служит в языке C# в качестве контекстного ключевого слова. Это означает, что оно имеет специальное назначение только в блоке итератора. А вне этого блока оно может быть использовано аналогично любому другому идентификатору. Следует особо подчеркнуть, что итератор не обязательно должен опираться на мас сив или коллекцию другого типа. Он должен просто возвращать следующий элемент из совокупности элементов. Это означает, что элементы могут быть построены дина мически с помощью соответствующего алгоритма. В качестве примера ниже приведе на версия предыдущей программы, в которой возвращаются все буквы английского алфавита, набранные в верхнем регистре. Вместо массива буквы формируются в цикле for. // Пример динамического построения значений, // возвращаемых по очереди с помощью итератора. using System; using System.Collections; class MyClass { char ch = 'A'; // Этот итератор возвращает буквы английского // алфавита, набранные в верхнем регистре. public IEnumerator GetEnumerator() { for(int i=0; i < 26; i++) yield return (char) (ch + i); } } class ItrDemo2 { static void Main() { MyClass me = new MyClass(); foreach(char ch in me) Console.Write(ch + " "); Console.WriteLine(); } } Вот к какому результату приводит выполнение этой программы. A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Прерывание итератора Для преждевременного прерывания итератора служит следующая форма опера тора yield. yield break; Когда этот оператор выполняется, итератор уведомляет о том, что достигнут конец коллекции. А это, по существу, останавливает сам итератор. Приведенная ниже программа является версией предыдущей программы, изме ненной с целью отобразить только первые десять букв английского алфавита. // Пример прерывания итератора. using System; using System.Collections; class MyClass { char ch = 'A'; // Этот итератор возвращает первые 10 букв английского алфавита. public IEnumerator GetEnumerator() { for(int i=0; i < 26; i++) { if(i == 10) yield break; // прервать итератор преждевременно yield return (char) (ch + i); } } } class ItrDemo3 { static void Main() { MyClass mc = new MyClass(); foreach(char ch in mc) Console.Write(ch + " "); Console.WriteLine(); } } Эта программа дает следующий результат. A B C D E F G H I J Применение нескольких операторов yield В итераторе допускается применение нескольких операторов yield. Но каждый такой оператор должен возвращать следующий элемент в коллекции. В качестве при мера рассмотрим следующую программу. // Пример применения нескольких операторов yield. using System; using System.Collections; class MyClass { // Этот итератор возвращает буквы А, В, С, D и Е. public IEnumerator GetEnumerator() { yield return 'A'; yield return 'B'; yield return 'C'; yield return 'D'; yield return 'E'; } } class ItrDemo5 { static void Main() { MyClass me = new MyClass (); foreach(char ch in mc) Console.Write(ch + " "); Console.WriteLine(); } } Ниже приведен результата выполнения этой программы. А В С D Е В данной программе внутри метода GetEnumerator() выполняются пять опера торов yield. Следует особо подчеркнуть, что они выполняются по очереди и каждый раз, когда из коллекции получается очередной элемент. Таким образом, на каждом шаге цикла foreach в методе Main() возвращается только один символ. Создание именованного итератора В приведенных выше примерах был продемонстрирован простейший способ ре ализации итератора. Но ему имеется альтернатива в виде именованного итератора. В данном случае создается метод, оператор или аксессор, возвращающий ссылку на объект типа IEnumerable. Именно этот объект используется в коде для предоставле ния итератора. Именованный итератор представляет собой метод, общая форма кото рого приведена ниже: public IEnumerable имя