Полное руководство. С# 4.0 - Шилдт Герберт
Шрифт:
Интервал:
Закладка:
Получить перечислитель, устанавливаемый в начало коллекции, вызвав для этойколлекции метод 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 90 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 имяитератора(списокпараметров) {// ...yield return obj;}где имяитератора обозначает конкретное имя метода; списокпараметров — отнуля до нескольких параметров, передаваемых методу итератора; obj — следующийобъект, возвращаемый итератором. Как только именованный итератор будет создан,его можно использовать везде, где он требуется, например для управления цикломforeach.Именованные итераторы оказываются весьма полезными в некоторых ситуациях,поскольку они позволяют передавать аргументы итератору, управляющему процессом получения конкретных элементов из коллекции. Например, итератору можнопередать начальный и конечный пределы совокупности элементов, возвращаемых изколлекции итератором. Эту форму итератора можно перегрузить, расширив ее функциональные возможности. В приведенном ниже примере программы демонстрируются два способа применения именованного итератора для получения элементов коллекции. В одном случае элементы перечисляются в заданных начальном и конечномпределах, а в другом — элементы перечисляются с начала последовательности и доуказанного конечного предела.// Использовать именованные итераторы.using System;using System.Collections;class MyClass {char ch = 'A';// Этот итератор возвращает буквы английского алфавита,// начиная с буквы А и кончая указанным конечным пределом.public IEnumerable MyItr(int end) {for(int i=0; i < end; i++)yield return (char) (ch + i);}// Этот итератор возвращает буквы в заданных пределах.public IEnumerable MyItr(int begin, int end) {for(int i=begin; i < end; i++)yield return (char) (ch + i);}}class ItrDemo4 {static void Main() {MyClass mc = new MyClass();Console.WriteLine("Возвратить по очереди первые 7 букв:");foreach(char ch in mc.MyItr(7))Console.Write(ch + " ");Console.WriteLine("n");Console.WriteLine("Возвратить по очереди буквы от F до L:");foreach(char ch in mc.MyItr(5, 12))Console.Write(ch + " ");Console.WriteLine();}}Эта программа дает следующий результат.Возвратить по очереди первые 7 букв:А В С D Е F GВозвратить по очереди буквы от F до L:F G Н I J К LСоздание обобщенного итератораВ приведенных выше примерах применялись необобщенные итераторы, но, конечно, ничто не мешает создать обобщенные итераторы. Для этого достаточно возвратитьобъект обобщенного типа IEnumerator или IEnumerable. Ниже приведенпример создания обобщенного итератора.// Простой пример обобщенного итератора.using System;using System.Collections.Generic;class MyClass {T[] array;public MyClass(T[] a) {array = a;}// Этот итератор возвращает символы из массива chrs.public IEnumerator GetEnumerator() {foreach(T obj in array)yield return obj;}}class GenericItrDemo {static void Main() {int[] nums = { 4, 3, 6, 4, 7, 9 };MyClass me = new MyClass(nums);foreach(int x in mc)Console.Write(x + " ");Console.WriteLine();bool[] bVals = { true, true, false, true };MyClass mc2 = new MyClass(bVals);foreach(bool b in mc2)Console.Write(b + " ");Console.WriteLine();}}Вот к какому результату приводит выполнение этой программы.4 3 6 4 7 9True True False TrueВ данном примере массив, состоящий из возвращаемых по очереди объектов, передается конструктору класса MyClass. Тип этого массива указывает в качестве аргумента типа в конструкторе класса MyClass.Метод GetEnumerator() оперирует данными обобщенного типа т и возвращает перечислитель типа IEnumerator. Следовательно, итератор, определенный вклассе MyClass, способен перечислять данные любого типа.Инициализаторы коллекцийВ C# имеется специальное средство, называемое инициализатором коллекции и упрощающее инициализацию некоторых коллекций. Вместо того чтобы явно вызывать метод Add(), при создании коллекции можно указать список инициализаторов. Послеэтого компилятор организует автоматические вызовы метода Add(), используя значения из этого списка. Синтаксис в данном случае ничем не отличается от инициализации массива. Обратимся к следующему примеру, в котором создается коллекция типаList, инициализируемая символами С, А, Е, В, D и F.List lst = new List() { 'С', 'А', 'Е', 'В', 'D', 'F' };После выполнения этого оператора значение свойства lst.Count будет равно 6,поскольку именно таково число инициализаторов. А после выполнения следующегоцикла foreach:foreach(ch in lst)Console.Write(ch + " ");получится такой результат:С A E В D FДля инициализации коллекции типа LinkedList, в которой хранятся пары "ключ-значение", инициализаторы приходится предоставлять парами, какпоказано ниже.SortedList lst =new SortedList() { {1, "один"}, {2, "два" }, {3, "три"} };Компилятор передаст каждую группу значений в качестве аргументов методуAdd(). Следовательно, первая пара инициализаторов преобразуется компиляторомв вызов Add(1, "один").Компилятор вызывает метод Add() автоматически для ввода инициализаторов вколлекцию, и поэтому инициализаторы коллекций можно использовать только в коллекциях, поддерживающих открытую реализацию метода Add(). Это означает, чтоинициализаторы коллекций нельзя использовать в коллекциях типа Stack, Stack,Queue или Queue, поскольку в них метод Add() не поддерживается. Их нельзяприменять также в тех коллекциях типа LinkedList, где метод Add() предоставляется как результат явной реализации соответствующего интерфейса.