Прочитав эту главу, вы научитесь:
• самостоятельно определять нумератор с возможностью его использования для последовательного обхода элементов коллекции;
• создавать нумератор в автоматическом режиме путем создания итератора;
• предоставлять дополнительные итераторы для пошагового обхода элементов коллекции в различных последовательностях.
В главе 10 «Использование массивов» и в главе 18 «Использование коллекций» было показано, как работать с массивами и классами коллекций для хранения последовательностей или наборов данных. В главе 10 также подробно рассмотрена инструкция foreach, которую можно использовать для пошагового обхода или перебора элементов в коллекции. В этих главах инструкция foreach использовалась в качестве быстрого и удобного способа доступа к содержимому массива или коллекции, но теперь настало время получить больше сведений о том, как работает эта инструкция. Эта тема приобретает важность при определении ваших собственных классов коллекций, и в данной главе рассказывается, как сделать коллекции перечисляемыми.
В главе 10 представлен пример использования инструкции foreach для перечисления элементов в простом массиве. Код выглядит следующим образом:
int[] pins = { 9, 3, 7, 2 };
foreach (int pin in pins)
{
Console.WriteLine(pin);
}
Конструктор foreach предоставляет весьма изящный механизм, существенно упрощающий создаваемый код, но он может быть применен только при определенных обстоятельствах: им можно воспользоваться только для пошагового обхода перечисляемой коллекции.
Но что на самом деле представляет собой перечисляемая коллекция? Если давать краткое определение, то это коллекция, реализующая интерфейс System.Collections.IEnumerable.
ПРИМЕЧАНИЕ Следует помнить, что все массивы в C# фактически являются экземплярами класса System.Array. Этот класс является классом коллекции, реализующим интерфейс IEnumerable.
В интерфейсе IEnumerable содержится всего один метод GetEnumerator:
IEnumerator GetEnumerator();
Этот метод должен возвратить объект-нумератор, реализующий интерфейс System.Collections.IEnumerator. Объект-нумератор используется для пошагового обхода (перечисления) элементов коллекции. Интерфейс IEnumerator определяет следующие свойства и методы:
object Current { get; }
bool MoveNext();
void Reset();
Нумератор следует представлять в виде указателя на элементы списка. В исходном состоянии указатель стоит перед первым элементом. Для перемещения указателя на следующий (первый) элемент списка нужно вызвать метод MoveNext, который должен возвратить true, если следующий элемент имеется в списке, и false, если такого элемента в нем нет. Для доступа к элементу, на котором стоит указатель, используется свойство Current, а для возвращения указателя назад и установки его перед первым элементом списка — метод Reset. Использованием для нумератора метода коллекции GetEnumerator выполняется многократный вызов метода MoveNext, а применяя нумератор для извлечения значения свойства Current, можно пошагово продвигаться вперед по элементам. Именно этим и занимается инструкция foreach. Следовательно, если нужно создать свой перечисляемый класс коллекции, вы должны реализовать в коллекции интерфейс IEnumerable, а также предоставить реализацию интерфейса IEnumerator для его возвращения методом GetEnumerator класса коллекции.
ВНИМАНИЕ На первый взгляд интерфейсы IEnumerable и IEnumerator имеют очень похожие имена, в которых несложно запутаться. Убедитесь в том, что вы их не перепутали.
Если присмотреться, можно заметить, что свойством Current интерфейса IEnumerator демонстрируется поведение, не обеспечивающее безопасности по отношению к типам, поскольку им возвращается объект, а не конкретный тип. Но вам, вероятно, интересно будет узнать, что библиотека классов Microsoft .NET Framework также предоставляет интерфейс-обобщение IEnumerator<T>, у которого имеется свойство Current, возвращающее вместо этого T. Также в этой библиотеке имеется интерфейс IEnumerable<T>, содержащий метод GetEnumerator, который возвращает объект Enumerator<T>. Оба этих интерфейса определены в пространстве имен System.Collections.Generic, и если вы создаете приложение для .NET Framework версии 2.0 и выше, то при определении перечисляемых коллекций следует воспользоваться именно этими интерфейсами-обобщениями, а не версиями, не являющимися обобщениями.
В следующем упражнении вами будет определен класс, реализующий интерфейс-обобщение IEnumerator<T> и создающий нумератор для класса двоичного дерева, который рассмотрен в главе 17 «Введение в обобщения».
В главе 17 было показано, как можно без особых трудностей выполнить обход двоичного дерева и вывести на экран его содержимое. Из-за этого может возникнуть мнение, что определение нумератора, извлекающего каждый элемент в двоичном дереве в том же порядке, станет довольно простой задачей. Как ни печально, но это не так. Основная проблема заключается в том, что при определении нумератора нужно помнить, что вы находитесь в структуре и поэтому последовательные вызовы метода MoveNext могут соответственно обновить позицию. Рекурсивные алгоритмы, подобные тому, что используется для обхода двоичного дерева, сами по себе не предоставляют простым и доступным образом сохраняемую информацию о состоянии между вызовами метода. По этой причине вы сначала проведете предварительную обработку данных в двоичном дереве, превращая его в более податливую структуру данных (а именно в очередь), а взамен сделаете эту структуру перечисляемой. Разумеется, эта хитрость будет скрыта от пользователя, совершающего обход элементов двоичного дерева!
Откройте в среде Microsoft Visual Studio 2015 решение BinaryTree, которое находится в папке \Microsoft Press\VCSBS\Chapter 19\BinaryTree вашей папки документов. В этом решении содержится рабочая копия проекта BinaryTree, созданного в главе 17. Вы добавите к этому проекту новый класс, в котором будет реализован нумератор для класса BinaryTree.
Щелкните в обозревателе решений на проекте BinaryTree. Щелкните в меню Проект на пункте Добавить класс, чтобы открыть диалоговое окно Добавить новый элемент — BinaryTree. Выберите в средней панели шаблон Класс, наберите в поле Имя строку TreeEnumerator.cs, а затем щелкните на кнопке Добавить. Будет создан класс TreeEnumerator, создающий нумератор для Tree<TItem>-объекта. Чтобы гарантировать классу безопасность в отношении типов, следует предоставить параметр типа и реализовать IEnumerator<T>-интерфейс. Кроме того, параметр типа должен быть допустимым типом для Tree<TItem>-объекта, который класс будет превращать в перечисляемый, поэтому он должен быть принужден к реализации IComparable<TItem>-интерфейса (класс BinaryTree требует, чтобы для сортировки элементы дерева предоставляли средства, позволяющие сравнивать их друг с другом).
Измените в окне редактора, показывающего содержимое файла TreeEnumerator.cs, определение класса TreeEnumerator, чтобы оно отвечало этим требованиям. Соответствующие изменения показаны в следующем примере жирным шрифтом:
class TreeEnumerator<TItem> : IEnumerator<TItem> where TItem : IComparable<TItem>
{
}
Добавьте к классу TreeEnumerator<TItem> следующие три закрытые переменные, выделенные здесь жирным шрифтом:
class TreeEnumerator<TItem> : IEnumerator<TItem> where TItem : IComparable<TItem>
{
private Tree<TItem> currentData = null;
private TItem currentItem = default(TItem);
private Queue<TItem> enumData = null;
}
Переменная currentData будет использоваться для хранения ссылки на дерево, превращаемое в перечисляемое, а переменная currentItem — для хранения значения, возвращаемого свойством Current. Очередь enumData будет заполняться значениями, извлекаемыми из узлов дерева, а метод MoveNext будет по порядку возвращать каждый элемент из этой очереди. Значение ключевого слова default объясняется в этой главе чуть позже во врезке «Инициализация переменной, определяемой с параметром типа».
Добавьте конструктор, который забирает в класс TreeEnumerator<TItem> Tree<TItem>-параметр по имени data. Добавьте в тело конструктора инструкцию, инициализирующую переменную currentData значением data:
class TreeEnumerator<TItem> : IEnumerator<TItem> where TItem : IComparable<TItem>
{
...
public TreeEnumerator(Tree<TItem> data)
{
this.currentData = data;
}
}
Добавьте к классу TreeEnumerator<TItem> сразу же после конструктора следующий закрытый метод populate:
class TreeEnumerator<TItem> : IEnumerator<TItem> where TItem : IComparable<TItem>
{
...
private void populate(Queue<TItem> enumQueue, Tree<TItem> tree)
{
if (tree.LeftTree != null)
{
populate(enumQueue, tree.LeftTree);
}
enumQueue.Enqueue(tree.NodeData);
if (tree.RightTree != null)
{
populate(enumQueue, tree.RightTree);
}
}
}
Этот метод совершает обход двоичного дерева, добавляя содержащиеся в нем данные в очередь. В нем используется почти такой же алгоритм, как и тот, что использовался в методе WalkTree класса Tree<TItem>, описание которого приводилось в главе 17. Основное отличие заключается в том, что вместо добавления значений NodeData к строке метод сохраняет их в очереди.
Вернитесь к определению класса TreeEnumerator<TItem>. Поставьте в определении класса указатель мыши над текстом IEnumerator<TItem>. Щелкните в появившемся раскрывающемся контекстном меню (со значком горящей лампочки) на пункте Реализовать интерфейс явно. Это действие приведет к созданию заглушек для методов в интерфейсе IEnumerator<TItem> и интерфейсе IEnumerator, а также к добавлению его к концу класса. Будет также создан метод Dispose для интерфейса IDisposable.
ПРИМЕЧАНИЕ Интерфейс IEnumerator<TItem> наследуется из интерфейсов IEnumerator и IDisposable, из-за чего в нем появляются и их методы. Фактически единственным элементом, принадлежащим интерфейсу IEnumerator<TItem>, является свойство-обобщение Current. Методы MoveNext и Reset принадлежат необобщенному интерфейсу IEnumerator. А описание интерфейса IDisposable можно найти в главе 14 «Использование сборщика мусора и управление ресурсами».
Исследуйте созданный код. В телах свойств и методов содержится исходная реализация, которая просто выдает исключение NotImplementedException. На следующих этапах вы замените этот код реальной реализацией.
Обновите тело метода MoveNext кодом, выделенным здесь жирным шрифтом:
bool IEnumerator.MoveNext()
{
if (this.enumData == null)
{
this.enumData = new Queue<TItem>();
populate(this.enumData, this.currentData);
}
if (this.enumData.Count > 0)
{
this.currentItem = this.enumData.Dequeue();
return true;
}
return false;
}
Принадлежащий нумератору метод MoveNext выполняет двойную задачу. При первом вызове он должен инициализировать данные, используемые нумератором, и перейти к первой части данных, которые должны быть возвращены. (До того как метод MoveNext будет вызван в первый раз, значение, возвращаемое свойством Current, будет неопределенным, что должно привести к выдаче исключения.) В данном случае процесс инициализации состоит из создания экземпляра очереди с последующим вызовом метода populate для заполнения очереди данными, извлеченными из дерева.
Все последующие вызовы метода MoveNext должны просто приводить к перемещению по элементам данных, пока их уже не останется, с извлечением элементов из очереди, пока она, как в данном примере, не опустеет. Важно понимать, что фактически MoveNext элементы данных не возвращает, этим занимается свойство Current. Метод MoveNext лишь обновляет внутреннее состояние нумератора (то есть устанавливает для переменной currentItem значение элемента данных, извлеченного из очереди) с целью его использования свойством Current, возвращая при этом true при наличии следующего значения и false — при его отсутствии.
Внесите в определение метода доступа get свойства-обобщения Current следующие изменения, выделенные жирным шрифтом:
TItem IEnumerator<TItem>.Current
{
get
{
if (this.enumData == null)
{
throw new InvalidOperationException("Use MoveNext before calling
Current");
}
return this.currentItem;
}
}
Инициализация переменной, определяемой с параметром типа
Вы, наверное, заметили, что инструкция, определяющая и инициализирующая переменную currentItem, использует ключевое слово default. Переменная currentItem определяется с помощью параметра типа TItem. Когда программа написана и скомпилирована, реальный тип, который будет подставлен вместо TItem, может быть неизвестен — этот вопрос решается только в ходе выполнения кода. Это затрудняет указание на способ инициализации переменной. Возникает соблазн установить для нее значение null. Но если тип, подставляемый вместо TItem, является типом значений, такое присваивание будет недопустимым. (Для типов значений нельзя устанавливать значение null, это можно делать только для ссылочных типов.) Аналогично этому, если установить для этой переменной значение 0, предполагая при этом, что тип будет числовым, это значение будет недопустимо, если в реальности будет использоваться ссылочный тип. Но есть и другая возможность — вместо TItem может подставляться, к примеру, булев тип. Эта проблема решается с помощью ключевого слова default. Значение, используемое для инициализации переменной, будет определяться при выполнении инструкции. Если TItem становится ссылочным типом, то default(TItem) возвращает null, если TItem становится числовым типом, то default(TItem) возвращает 0, а если TItem становится булевым типом, то default(TItem) возвращает false. Если же TItem становится структурой, то отдельные поля в структуре инициализируются таким же образом. (Ссылочные поля устанавливаются в null, числовые поля — в 0, а булевы поля — в false.)
ВНИМАНИЕ Код следует добавить к соответствующей реализации свойства Current. А необобщенную версию System.Collections.IEnumerator.Current нужно оставить в исходном состоянии, выдающем исключение NotImplementedException.
Чтобы убедиться, что был вызван метод MoveNext, свойство Current исследует переменную enumData. (Эта переменная до первого вызова MoveNext будет иметь значение null.) Если вызова не было, свойство выдает исключение InvalidOperationException, что является общепринятым механизмом, используемым приложениями .NET Framework в качестве оповещения о том, что операция в текущем состоянии не может быть выполнена. Если предварительно был вызван метод MoveNext, то он уже обновил значение переменной currentItem, поэтому свойству Current остается лишь возвратить значение этой переменной.
Найдите метод IDisposable.Dispose. Закомментируйте строку throw new NotImplementedException();, выделенную в следующем примере кода жирным шрифтом. Нумератор не использует какие-либо ресурсы, требующие явного высвобождения, поэтому данному методу ничего не нужно делать. Но он все же должен присутствовать. Дополнительные сведения о методе Dispose можно найти в главе 14.
void IDisposable.Dispose()
{
// throw new NotImplementedException();
}
Выполните сборку решения и исправьте ошибки, если о них будут выданы сообщения.
В следующем упражнении вам предстоит внести изменения в класс двоичного дерева, чтобы реализовать в нем интерфейс IEnumerable<T>. Объект TreeEnumerator<TItem> будет возвращаться методом GetEnumerator.
В обозревателе решений дважды щелкните на файле Tree.cs, чтобы в окне редактора отобразился класс Tree<TItem>. Внесите в определение класса Tree<TItem> изменения, выделенные в следующем примере кода жирным шрифтом, чтобы в нем реализовывался интерфейс IEnumerable<TItem>:
public class Tree<TItem> : IEnumerable<TItem> where TItem : IComparable<TItem>
Обратите внимание на то, что ограничения всегда помещаются в конце определения класса.
В определении класса наведите указатель мыши на интерфейс IEnumerable<TItem>. В раскрывающемся контекстном меню щелкните на пункте Реализовать интерфейс явно. В результате этого будет создана реализация методов IEnumerable<TItem>.GetEnumerator и IEnumerable.GetEnumerator, которая будет добавлена к классу. Не являющийся обобщением метод IEnumerable этого интерфейса реализуется за счет того, что интерфейс-обобщение IEnumerable<TItem> является наследником интерфейса IEnumerable.
Найдите метод-обобщение IEnumerable<TItem>.GetEnumerator, который находится в нижней части кода класса. Внесите изменения, показанные в следующем примере кода жирным шрифтом, в тело метода GetEnumerator(), заменив новой строкой существующую инструкцию throw:
IEnumerator<TItem> IEnumerable<TItem>.GetEnumerator()
{
return new TreeEnumerator<TItem>(this);
}
Целью метода GetEnumerator является создание объекта-нумератора для сквозного обхода элементов коллекции. В данном случае вам нужно лишь создать новый TreeEnumerator<TItem>-объект, используя данные, находящиеся в дереве.
Выполните сборку решения и исправьте ошибки, если о них будут выданы сообщения.
Теперь вы протестируете измененный класс Tree<TItem>, используя для сквозного обхода элементов двоичного дерева и вывода на экран его содержимого инструкцию foreach.
В обозревателе решения щелкните правой кнопкой мыши на решении BinaryTree, укажите на пункт Добавить, а затем щелкните на пункте Создать проект. Добавьте новый проект, воспользовавшись шаблоном Консольное приложение. В качестве его имени укажите EnumeratorTest, в качестве места размещения — папку \Microsoft Press\VCSBS\Chapter 19\BinaryTree своей папки документов, после чего щелкните на кнопке OK.
ПРИМЕЧАНИЕ Убедитесь в том, что в списке шаблонов Visual C# выбран именно шаблон Консольное приложение. В диалоговом окне Добавить новый проект изначально отображаются шаблоны для Visual Basic или C++.
В обозревателе решений щелкните правой кнопкой мыши на проекте EnumeratorTest, а затем щелкните на пункте Назначить автозагружаемым проектом.
Щелкните в меню Проект на пункте Добавить ссылку. Раскройте в левой панели диалогового окна Менеджер ссылок — EnumeratorTest узел Проекты и щелкните на пункте Решение. Установите флажок возле проекта BinaryTree, а затем щелкните на кнопке OK. В перечне ссылок для проекта EnumeratorTest в обозревателе решений появится сборка BinaryTree.
Выведите в окно редактора содержимое класса Program и добавьте к списку в начале файла следующую директиву using:
using BinaryTree;
Добавьте к методу Main инструкции, выделенные в следующем примере кода жирным шрифтом. Эти инструкции создают двоичное дерево и заполняют его целочисленными значениями:
static void Main(string[] args)
{
Tree<int> tree1 = new Tree<int>(10);
tree1.Insert(5);
tree1.Insert(11);
tree1.Insert(5);
tree1.Insert(-12);
tree1.Insert(15);
tree1.Insert(0);
tree1.Insert(14);
tree1.Insert(-8);
tree1.Insert(10);
}
Добавьте выделенную далее жирным шрифтом инструкцию foreach, которая займется перечислением содержимого дерева и выводом результатов на экран:
static void Main(string[] args)
{
...
foreach (int item in tree1)
{
Console.WriteLine(item);
}
}
Щелкните в меню Отладка на пункте Запуск без отладки. Программа запустится и выведет на экран значения в следующей последовательности (рис. 19.1):
–12, –8, 0, 5, 5, 10, 10, 11, 14, 15
Рис. 19.1
Нажмите Ввод, чтобы вернуться в среду Visual Studio 2015.
Вполне очевидно, что процесс превращения коллекции в перечисляемую может усложниться и он не застрахован от ошибок. Чтобы упростить решение задачи, C# предоставляет итераторы, способные во многом автоматизировать ход этого процесса.
Итератор представляет собой блок кода, выдающий упорядоченную последовательность значений. Итератор не является компонентом перечисляемого класса, а указывает последовательность, которую нумератор должен использовать для возвращения своих значений. Иными словами, итератор — это просто описание последовательности перечисления, которой компилятор C# может воспользоваться для создания собственного нумератора. Усвоение этой концепции требует некоторого осмысления, поэтому давайте рассмотрим следующий простой пример.
Принципы реализации итератора будут проиллюстрированы на примере класса BasicCollection<T>. Для хранения данных и предоставления метода FillList для заполнения списка в классе используется List<T>-объект. Обратите внимание также на то, что что в классе BasicCollection<T> реализуется интерфейс IEnumerable<T>. Метод GetEnumerator реализуется с помощью итератора:
using System;
using System.Collections.Generic;
using System.Collections;
class BasicCollection<T> : IEnumerable<T>
{
private List<T> data = new List<T>();
public void FillList(params T [] items)
{
foreach (var datum in items)
{
data.Add(datum);
}
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
foreach (var datum in data)
{
yield return datum;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
// В данном примере не реализован
throw new NotImplementedException();
}
}
При всей своей очевидной простоте метод GetEnumerator заслуживает более тщательного исследования. Первое, на что следует обратить внимание: он не возвращает тип IEnumerator<T>. Вместо этого он осуществляет циклический обход элементов в массиве данных, возвращая по очереди каждый элемент. Основным моментом является использование ключевого слова yield. Оно указывает на значение, которое должно быть возвращено каждой итерацией. Если это вам поможет, то можете считать, что инструкция yield временно останавливает метод, возвращая значение вызвавшему его коду. Когда этому коду требуется следующее значение, метод GetEnumerator продолжает работу с того места, на котором она прервалась, осуществляет новый проход цикла, после чего выдает следующее значение. Со временем данные будут исчерпаны, цикл финиширует и метод GetEnumerator закончит свою работу. На этом итерация будет завершена.
Запомните, что в обычном понимании этот метод нельзя назвать нормальным. Код в методе GetEnumerator определяется как итератор. Компилятор использует этот код для создания реализации класса IEnumerator<T>, содержащего методы Current и MoveNext. Эта реализация в точности соответствует тем функциональным возможностям, которые указаны методом GetEnumerator. Но увидеть этот сгенерированный код вам, скорее всего, не удастся (если только не декомпилировать сборку, содержащую скомпилированный код), однако это не такая уж высокая плата за удобство и сокращение объема кода, который вам необходимо создать. Нумератор, созданный итератором, можно вызвать обычным образом, что и показано в следующем блоке кода, который выводит на экран слова первой строки стихотворения Льюиса Кэрролла «Бармаглот»:
BasicCollection<string> bc = new BasicCollection<string>();
bc.FillList("Twas", "brillig", "and", "the", "slithy", "toves");
foreach (string word in bc)
{
Console.WriteLine(word);
}
Этот код просто выводит содержимое объекта bc в следующем порядке:
Twas, brillig, and, the, slithy, toves
Если для представления данных в другой последовательности нужен альтернативный механизм итерации, можно создать дополнительные свойства, в которых реализуется интерфейс IEnumerable, и использовать для возвращения данных итератор. Например, показанное здесь свойство Reverse класса BasicCollection<T> выдает данные, находящиеся в списке, в обратном порядке:
class BasicCollection<T> : IEnumerable<T>
{
...
public IEnumerable<T> Reverse
{
get
{
for (int i = data.Count - 1; i >= 0; i--)
{
yield return data[i];
}
}
}
}
Это свойство можно вызвать следующим образом:
BasicCollection<string> bc = new BasicCollection<string>();
bc.FillList("Twas", "brillig", "and", "the", "slithy", "toves");
foreach (string word in bc.Reverse)
{
Console.WriteLine(word);
}
Этот код выводит на экран содержимое объекта bc в обратном порядке:
toves, slithy, the, and, brillig, Twas
В следующем упражнении нумератор для класса Tree<TItem> будет реализован с использованием итератора. В отличие от предыдущего набора упражнений, требовавшего предварительной обработки данных, находящихся в дереве, и помещения их в очередь с использованием метода MoveNext, здесь вы можете определить итератор, обеспечивающий последовательный обход элементов дерева, путем использования более естественного рекурсивного механизма, похожего на тот, что использовался в методе WalkTree, рассмотренном в главе 17.
Откройте в среде Visual Studio 2015 решение BinaryTree, которое находится в папке \Microsoft Press\VCSBS\Chapter 19\IteratorBinaryTree вашей папки документов. Это решение содержит еще одну копию проекта BinaryTree, созданного в главе 17.
Откройте в окне редактора файл Tree.cs. Внесите в определение класса Tree<TItem> следующее изменение, выделенное здесь жирным шрифтом, показывающее, что в нем реализуется интерфейс IEnumerable<TItem>:
public class Tree<TItem> : IEnumerable<TItem> where TItem : IComparable<TItem>
{
...
}
Наведите указатель мыши на обозначенный в определении класса интерфейс IEnumerable<TItem>. Щелкните в появившемся контекстном меню на пункте Реализовать интерфейс явно, чтобы добавить к концу класса методы IEnumerable<TItem>.GetEnumerator и IEnumerable.GetEnumerator.
Найдите метод-обобщение IEnumerable<TItem>.GetEnumerator. Замените содержимое метода GetEnumerator кодом, выделенным здесь жирным шрифтом:
IEnumerator<TItem> IEnumerable<TItem>.GetEnumerator()
{
if (this.LeftTree != null)
{
foreach (TItem item in this.LeftTree)
{
yield return item;
}
}
yield return this.NodeData;
if (this.RightTree != null)
{
foreach (TItem item in this.RightTree)
{
yield return item;
}
}
}
На первый взгляд, может быть, это и не будет очевидным, но этот код следует такому же рекурсивному алгоритму, который использовался в главе 17 для обхода содержимого двоичного дерева. Если значение у LeftTree не пустое, первая инструкция foreach неявным образом вызывает в отношении себя же метод GetEnumerator, который именно сейчас и определяется. Этот процесс продолжается до тех пор, пока не будет найден узел без левого поддерева. В этот момент свойство NodeData уже возвращено, и таким же образом выполняется исследование правого поддерева. Когда данные в правом поддереве будут исчерпаны, процесс отматывается до родительского узла, возвращая родительское свойство NodeData и исследуя правое поддерево родительского узла. Такой порядок действий соблюдается до тех пор, пока процесс нумерации не охватит все дерево и не будут возвращены все узлы.
В обозревателе решений щелкните правой кнопкой мыши на решении BinaryTree, укажите на пункт Добавить и щелкните на пункте Существующий проект. В диалоговом окне Добавить существующий проект перейдите в папку \Microsoft Press\VCSBS\Chapter 19\BinaryTree\EnumeratorTest, выберите файл проекта EnumeratorTest, а затем щелкните на кнопке Открыть. Это проект, созданный для тестирования нумератора, который ранее в этой главе вы разработали собственными силами.
В обозревателе решений щелкните правой кнопкой мыши на проекте EnumeratorTest, а затем щелкните на пункте Назначить автозагружаемым проектом. Раскройте в обозревателе решений узел Ссылки, относящийся к проекту EnumeratorTest, щелкните правой кнопкой мыши на ссылке BinaryTree, после чего щелкните на пункте Удалить.
В меню Проект щелкните на пункте Добавить ссылку. Раскройте в левой панели диалогового окна Менеджер ссылок — EnumeratorTest узел Проекты и щелкните на пункте Решение. Установите в средней панели флажок возле проекта BinaryTree, а затем щелкните на кнопке OK.
ПРИМЕЧАНИЕ Эти два действия гарантируют вам, что проект EnumeratorTest ссылается на нужную версию сборки BinaryTree. Здесь должна использоваться сборка, реализующая нумератор путем использования итератора, а не та версия, которая была создана в предыдущем наборе упражнений данной главы.
Выведите в окно редактора файл Program.cs проекта EnumeratorTest. Посмотрите в этом файле на метод Main. Вы должны вспомнить, что при тестировании ранее созданного нумератора этот метод создавал экземпляр Tree<int>-объекта, наполнял его данными и затем, используя инструкцию foreach, выводил на экран его содержимое. Выполните сборку решения и исправьте ошибки, если о них будут выданы сообщения. Щелкните в меню Отладка на пункте Запуск без отладки. Программа запустится и выведет значения в том же порядке, что и раньше:
–12, –8, 0, 5, 5, 10, 10, 11, 14, 15
Нажмите Ввод и вернитесь в среду Visual Studio 2015.
В этой главе вы узнали, как для того, чтобы приложения могли выполнять сквозной обход элементов коллекции, в классе этой коллекции реализуются интерфейсы IEnumerable<T> и IEnumerator<T>. Вы также увидели, как нумератор реализуется с помощью итератора.
Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 20 «Отделение логики приложения и обработка событий».
Если сейчас вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Увидев диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.
Чтобы | Сделайте следующее |
Превратить класс коллекции в перечисляемый, позволяя ему поддерживать конструкцию foreach | Реализуйте интерфейс IEnumerable и предоставьте метод GetEnumerator, возвращающий объект IEnumerator, например: public class Tree<TItem> : IEnumerable<TItem> { ... IEnumerator<TItem> GetEnumerator() { ... } } |
Реализовать нумератор без использования итератора | Определите класс нумератора, реализующий интерфейс IEnumerator, а также предоставляющий свойство Current и метод MoveNext (и, возможно, дополнительно метод Reset), например: public class TreeEnumerator<TItem> : IEnumerator<TItem> { ... TItem Current { get { ... } }
bool MoveNext() { ... } } |
Определить нумератор путем использования итератора | Реализуйте нумератор для указания того, какие элементы должны быть возвращены (воспользовавшись инструкцией yield) и в каком порядке, например: IEnumerator<TItem> GetEnumerator() { for (...) { yield return ... } } |