ГЛАВА 25. Коллекции, перечислители и итераторы
В этой главе речь пойдет об одной из самых важных составляющих среды .NET Framework: коллекци ях. В С# коллекция представляет собой совокупность объектов. В среде .NET Framework имеется немало интер фейсов и классов, в которых определяются и реализуются различные типы коллекций. Коллекции упрощают реше ние многих задач программирования благодаря тому, что предлагают готовые решения для создания целого ряда типичных, но порой трудоемких для разработки структур данных. Например, в среду .NET Framework встроены кол лекции, предназначенные для поддержки динамических массивов, связных списков, стеков, очередей и хеш-таблиц. Коллекции являются современным технологическим сред ством, заслуживающим пристального внимания всех, кто программирует на С#. Первоначально существовали только классы необоб щенных коллекций. Но с внедрением обобщений в версии C# 2.0 среда .NET Framework была дополнена многими новыми обобщенными классами и интерфейсами. Благо даря введению обобщенных коллекций общее количество классов и интерфейсов удвоилось. Вместе с библиотекой распараллеливания задач (TPL) в версии 4.0 среды .NET Framework появился ряд новых классов коллекций, пред назначенных для применения в тех случаях, когда доступ к коллекции осуществляется из нескольких потоков. Нетруд но догадаться, что прикладной интерфейс Collections API составляет значительную часть среды .NET Framework. Кроме того, в настоящей главе рассматриваются два сред ства, непосредственно связанные с коллекциями: перечисли тели и итераторы. И те и другие позволяют поочередно обра щаться к содержимому класса коллекции в цикле foreach. Краткий обзор коллекций
Главное преимущество коллекций заключается в том, что они стандартизируют об работку групп объектов в программе. Все коллекции разработаны на основе набора четко определенных интерфейсов. Некоторые встроенные реализации таких интер фейсов, в том числе ArrayList, Hashtable, Stack и Queue, могут применяться в ис ходном виде и без каких-либо изменений. Имеется также возможность реализовать собственную коллекцию, хотя потребность в этом возникает крайне редко. В среде .NET Framework поддерживаются пять типов коллекций: необобщенные, специальные, с поразрядной организацией, обобщенные и параллельные. Необобщен ные коллекции реализуют ряд основных структур данных, включая динамический мас сив, стек, очередь, а также словари, в которых можно хранить пары "ключ-значение". В отношении необобщенных коллекций важно иметь в виду следующее: они опериру ют данными типа object. Таким образом, необобщенные коллекции могут служить для хранения данных любого типа, причем в одной коллекции допускается наличие разнотипных данных. Очевидно, что такие коллекции не типизированы, поскольку в них хранятся ссылки на данные типа object. Классы и интерфейсы необобщенных коллекций находятся в пространстве имен System.Collections. Специальные коллекции оперируют данными конкретного типа или же делают это каким-то особым образом. Например, имеются специальные коллекции для сим вольных строк, а также специальные коллекции, в которых используется однонаправ ленный список. Специальные коллекции объявляются в пространстве имен System. Collections.Specialized. В прикладном интерфейсе Collections API определена одна коллекция с поразряд ной организацией — это BitArray. Коллекция типа BitArray поддерживает пораз рядные операции, т.е. операции над отдельными двоичными разрядами, например И иди исключающее ИЛИ, а следовательно, она существенно отличается своими воз можностями от остальных типов коллекций. Коллекция типа BitArray объявляется в пространстве имен System.Collections. Обобщенные коллекции обеспечивают обобщенную реализацию нескольких стан дартных структур данных, включая связные списки, стеки, очереди и словари. Такие коллекции являются типизированными в силу их обобщенного характера. Это озна чает, что в обобщенной коллекции могут храниться только такие элементы данных, которые совместимы по типу с данной коллекцией. Благодаря этому исключается случайное несовпадение типов. Обобщенные коллекции объявляются в пространстве имен System.Collections.Generic. Параллельные коллекции поддерживают многопоточный доступ к коллекции. Это обобщенные коллекции, определенные в пространстве имен System.Collections. Concurrent. В пространстве имен System.Collections.ObjectModel находится также ряд классов, поддерживающих создание пользователями собственных обобщенных кол лекций. Основополагающим для всех коллекций является понятие перечислителя, который поддерживается в необобщенных интерфейсах IEnumerator и IEnumerable, а также в обобщенных интерфейсах IEnumerator и IEnumerable. Перечислитель обе спечивает стандартный способ поочередного доступа к элементам коллекции. Следо вательно, он перечисляет содержимое коллекции. В каждой коллекции должна быть реализована обобщенная или необобщенная форма интерфейса IEnumerable, поэто му элементы любого класса коллекции должны быть доступны посредством методов, определенных в интерфейсе IEnumerator или IEnumerator. Это означает, что, внеся минимальные изменения в код циклического обращения к коллекции одного типа, его можно использовать для аналогичного обращения к коллекции другого типа. Любопытно, что для поочередного обращения к содержимому коллекции в цикле foreach используется перечислитель. Основополагающим для всех коллекций является понятие перечислителя, который поддерживается в необобщенных интерфейсах IEnumerator и IEnumerable, а также в обобщенных интерфейсах IEnumerator и IEnumerable. Перечислитель обе спечивает стандартный способ поочередного доступа к элементам коллекции. Следо вательно, он перечисляет содержимое коллекции. В каждой коллекции должна быть реализована обобщенная или необобщенная форма интерфейса IEnumerable, поэто му элементы любого класса коллекции должны быть доступны посредством методов, определенных в интерфейсе IEnumerator или IEnumerator. Это означает, что, внеся минимальные изменения в код циклического обращения к коллекции одного типа, его можно использовать для аналогичного обращения к коллекции другого типа. Любопытно, что для поочередного обращения к содержимому коллекции в цикле foreach используется перечислитель. С перечислителем непосредственно связано другое средство, называемое итерато ром. Это средство упрощает процесс создания классов коллекций, например специ альных, поочередное обращение к которым организуется в цикле foreach. Итераторы также рассматриваются в этой главе. И последнее замечание: если у вас имеется некоторый опыт программирования на C++, то вам, вероятно, будет полезно знать, что классы коллекций по своей сути по добны классам стандартной библиотеки шаблонов (Standard Template Library — STL), определенной в C++. То, что в программировании на C++ называется контейнером, в программировании на C# называется коллекцией. Это же относится и к Java. Если вы знакомы с библиотекой Collections Framework для Java, то научиться пользоваться коллекциями в C# не составит для вас большого труда. В силу характерных отличий каждый из пяти типов коллекций (необобщенных, обобщенных, специальных, с поразрядной организацией и параллельных) будет рас смотрен далее в этой главе отдельно. Необобщенные коллекции Необобщенные коллекции вошли в состав среды .NET Framework еще в версии 1.0. Они определяются в пространстве имен System.Collections. Необобщенные коллек ции представляют собой структуры данных общего назначения, оперирующие ссылка ми на объекты. Таким образом, они позволяют манипулировать объектом любого типа, хотя и не типизированным способом. В этом состоит их преимущество и в то же вре мя недостаток. Благодаря тому что необобщенные коллекции оперируют ссылками на объекты, в них можно хранить разнотипные данные. Это удобно в тех случаях, когда тре буется манипулировать совокупностью разнотипных объектов или же когда типы хра нящихся в коллекции объектов заранее неизвестны. Но если коллекция предназначается для хранения объекта конкретного типа, то необобщенные коллекции не обеспечивают типовую безопасность, которую можно обнаружить в обобщенных коллекциях. Необобщенные коллекции определены в ряде интерфейсов и классов, реализую щих эти интерфейсы. Все они рассматриваются далее по порядку. Интерфейсы необобщенных коллекций В пространстве имен System.Collections определен целый ряд интерфейсов необобщенных коллекций. Начинать рассмотрение необобщенных коллекций следу ет именно с интерфейсов, поскольку они определяют функциональные возможности, которые являются общими для всех классов необобщенных коллекций. Интерфейсы, служащие опорой для необобщенных коллекций, сведены в табл. 25.1. Каждый из этих интерфейсов подробно описывается далее. Таблица 25.1. Интерфейсы необобщенных коллекций Интерфейс ICollection Интерфейс ICollection служит основанием, на котором построены все необобще нные коллекции. В нем объявляются основные методы и свойства для всех необобщен ных коллекций. Он также наследует от интерфейса IEnumerable. В интерфейсе ICollection определяются перечисленные ниже свойства. Свойство Count используется чаще всего, поскольку оно содержит количество эле ментов, хранящихся в коллекции на данный момент. Если значение свойства Count равно нулю, то коллекция считается пустой. В интерфейсе ICollection определяется следующий метод. void СоруТо(Array target, int startIdx) Интерфейс Описание ICollection Определяет элементы, которые должны иметь все необоб щенные коллекции IComparer Определяет метод Compare() для сравнения объектов, хра нящихся в коллекции IDictionary Определяет коллекцию, состоящую из пар "ключ-значение" IDictionaryEnumerator Определяет перечислитель для коллекции, реализующей ин терфейс IDictionary IEnumerable Определяет метод GetEnumerator(), предоставляющий перечислитель для любого класса коллекции IEnumerator Предоставляет методы, позволяющие получать содержимое коллекции по очереди IEqualityComparer Сравнивает два объекта на предмет равенства IHashCodeProvider Считается устаревшим. Вместо него следует использовать ин терфейс IEqualityComparer IList Определяет коллекцию, доступ к которой можно получить с по мощью индексатора IStructuralComparable Определяет метод CompareTo(), применяемый для струк турного сравнения IStructuralEquatable Определяет метод Equals(), применяемый для выяснения структурного, а не ссылочного равенства. Кроме того, опреде ляет метод GetHashCode() Метод СоруТо() копирует содержимое коллекции в массив target, начиная с эле мента, указываемого по индексу startIdx. Следовательно, метод СоруТо() обеспечи вает в C# переход от коллекции к стандартному массиву. Благодаря тому что интерфейс ICollection наследует от интерфейса IEnumerable, в его состав входит также единственный метод, определенный в интерфейсе IEnumerable. Это метод GetEnumerator(), объявляемый следующим образом. IEnumerator GetEnumerator() Он возвращает перечислитель для коллекции. Вследствие того же наследования от интерфейса IEnumerable в интерфей се ICollection определяются также четыре следующих метода расширения: AsParallel(), AsQueryable(), Cast() и OfType(). В частности, метод AsParallel() объявляется в классе System.Linq.ParallelEnumerable, метод AsQueryable() — в классе System.Linq.Queryable, а методы Cast() и OfType() — в классе System. Linq.Enumerable. Эти методы предназначены главным образом для поддержки LINQ, хотя их можно применять и в других целях. Интерфейс IList В интерфейсе IList объявляется такое поведение необобщенной коллекции, ко торое позволяет осуществлять доступ к ее элементам по индексу с отсчетом от нуля. Этот интерфейс наследует от интерфейсов ICollection и IEnumerable. Помимо методов, определенных в этих интерфейсах, в интерфейсе IList определяется ряд собственных методов. Все эти методы сведены в табл. 25.2. В некоторых из них преду сматривается модификация коллекции. Если же коллекция доступна только для чте ния или имеет фиксированный размер, то в этих методах генерируется исключение NotSupportedException. Таблица 25.2. Методы, определенные в интерфейсе IList Свойство Назначение int Count { get; } Содержит количество элементов в коллекции на дан ный момент bool IsSynchronized { get; } Принимает логическое значение true, если коллек ция синхронизирована, а иначе — логическое зна чение false. По умолчанию коллекции не синхро низированы. Но для большинства коллекций можно получить синхронизированный вариант object SyncRoot { get; } Содержит объект, для которого коллекция может быть синхронизирована Метод Описание int Add(object value) Добавляет объект value в вызывающую коллекцию. Возвращает индекс, по которому этот объект сохра няется void Clear() Удаляет все элементы из вызывающей коллекции bool Contains (object value) Возвращает логическое значение true, если вызы вающая коллекция содержит объект value, а ина че — логическое значение false Окончание табл. 25.2 Объекты добавляются в коллекцию типа IList вызовом метода Add(). Обрати те внимание на то, что метод Add() принимает аргумент типа object. А поскольку object является базовым классом для всех типов, то в необобщенной коллекции мо жет быть сохранен объект любого типа, включая и типы значений, в силу автоматиче ской упаковки и распаковки. Для удаления элемента из коллекции служат методы Remove() и RemoveAt(). В част ности, метод Remove() удаляет указанный объект, а метод RemoveAt() удаляет объект по указанному индексу. И для опорожнения коллекции вызывается метод Clear(). Для того чтобы выяснить, содержится ли в коллекции конкретный объект, вызыва ется метод Contains(). Для получения индекса объекта вызывается метод IndexOf(), а для вставки элемента в коллекцию по указанному индексу — метод Insert(). В интерфейсе IList определяются следующие свойства. bool IsFixedSize { get; } bool IsReadOnly { get; } Если коллекция имеет фиксированный размер, то свойство IsFixedSize содержит логическое значение true. Это означает, что в такую коллекцию нельзя ни вставлять элементы, ни удалять их из нее. Если же коллекция доступна только для чтения, то свойство IsReadOnly содержит логическое значение true. Это означает, что содержи мое такой коллекции не подлежит изменению. Кроме того, в интерфейсе IList определяется следующий индексатор. object this[int index] { get; set; } Этот индексатор служит для получения и установки значения элемента коллекции. Но его нельзя использовать для добавления в коллекцию нового элемента. С этой це лью обычно вызывается метод Add(). Как только элемент будет добавлен в коллекцию, он станет доступным посредством индексатора. Метод Описание int IndexOf(object value) Возвращает индекс объекта value, если этот объект содержится в вызывающей коллекции. Если же объект value не обнаружен, то метод возвращает значение -1 void Insert(int index, object value) Вставляет в вызывающую коллекцию объект value по индексу index. Элементы, находившиеся до это го по индексу index и дальше, смещаются вперед, чтобы освободить место для вставляемого объекта value void Remove(object value) Удаляет первое вхождение объекта value в вызыва ющей коллекции. Элементы, находившиеся до этого за удаленным элементом, смещаются назад, чтобы устранить образовавшийся “пробел" void RemoveAt(int index) Удаляет из вызывающей коллекции объект, располо женный по указанному индексу index. Элементы, находившиеся до этого за удаленным элементом, смещаются назад, чтобы устранить образовавшийся “пробел” Интерфейс IDictionary В интерфейсе IDictionary определяется такое поведение необобщенной кол лекции, которое позволяет преобразовать уникальные ключи в соответствующие зна чения. Ключ представляет собой объект, с помощью которого значение извлекается впоследствии. Следовательно, в коллекции, реализующей интерфейс IDictionary, хранятся пары "ключ-значение". Как только подобная пара будет сохранена, ее мож но извлечь с помощью ключа. Интерфейс IDictionary наследует от интерфейсов ICollection и IEnumerable. Методы, объявленные в интерфейсе IDictionary, све дены в табл. 25.3. Некоторые из них генерируют исключение ArgumentNullException при попытке указать пустой ключ, поскольку пустые ключи не допускаются. Таблица 25.3. Методы, определенные в интерфейсе IDictionary Для добавления пары "ключ-значение" в коллекцию типа IDictionary служит метод Add(). Обратите внимание на то, что ключ и его значение указываются отдель но. А для удаления элемента из коллекции следует указать ключ этого объекта при вызове метода Remove(). И для опорожнения коллекции вызывается метод Clear(). Для того чтобы выяснить, содержит ли коллекция конкретный объект, вызыва ется метод Contains() с указанным ключом искомого элемента. С помощью мето да GetEnumerator() получается перечислитель, совместимый с коллекцией типа IDictionary. Этот перечислитель оперирует парами "ключ-значение". В интерфейсе IDictionary определяются перечисленные ниже свойства. Следует иметь в виду, что ключи и значения, содержащиеся в коллекции, доступны в отдельных списках с помощью свойств Keys и Values. Кроме того, в интерфейсе IDictionary определяется следующий индексатор. object this[object key] { get; set; } Метод Описание void Add(object key, object value) void Clear() Добавляет в вызывающую коллекцию пару "ключ- значение”, определяемую параметрами key и value Удаляет все пары "ключ-значение" из вызывающей коллекции bool Contains(object key) Возвращает логическое значение true, если вызываю щая коллекция содержит объект key в качестве ключа, в противном случае — логическое значение false IDictionaryEnumerator GetEnumerator() Возвращает перечислитель для вызывающей коллек ции void Remove(object key) Удаляет из коллекции элемент, ключ которого равен зна чению параметра key Свойство Назначение bool IsFixedSize { get; } Принимает логическое значение true, если словарь имеет фиксированный размер bool IsReadOnly { get; } Принимает логическое значение true, если словарь до ступен только для чтения ICollection Keys { get; } Получает коллекцию ключей ICollection Values { get; } Получает коллекцию значений Этот индексатор служит для получения и установки значения элемента коллекции, а также для добавления в коллекцию нового элемента. Но в качестве индекса в данном случае служит ключ элемента, а не собственно индекс. Интерфейсы IEnumerable, IEnumerator и IDictionaryEnumerator Интерфейс IEnumerable является необобщенным, и поэтому он должен быть реализован в классе для поддержки перечислителей. Как пояснялось выше, интер фейс IEnumerable реализуется во всех классах необобщенных коллекций, посколь ку он наследуется интерфейсом ICollection. Ниже приведен единственный метод GetEnumerator(), определяемый в интерфейсе IEnumerable. IEnumerator GetEnumerator() Он возвращает коллекцию. Благодаря реализации интерфейса IEnumerable мож но также получать содержимое коллекции в цикле foreach. В интерфейсе IEnumerator определяются функции перечислителя. С помощью ме тодов этого интерфейса можно циклически обращаться к содержимому коллекции. Если в коллекции содержатся пары "ключ-значение" (словари), то метод GetEnumerator() возвращает объект типа IDictionaryEnumerator, а не типа IEnumerator. Интерфейс IDictionaryEnumerator наследует от интерфейса IEnumerator и вводит дополни тельные функции, упрощающие перечисление словарей. В интерфейсе IEnumerator определяются также методы MoveNext() и Reset() и свойство Current. Способы их применения подробнее описываются далее в этой главе. А до тех пор следует отметить, что свойство Current содержит элемент, полу чаемый в текущий момент. Метод MoveNext() осуществляет переход к следующему элементу коллекции, а метод Reset() возобновляет перечисление с самого начала. Интерфейсы IComparer и IEqualityComparer В интерфейсе IComparer определяется метод Compare() для сравнения двух объектов. int Compare(object х, object у) Он возвращает положительное значение, если значение объекта х больше, чем у объекта у; отрицательное — если значение объекта х меньше, чем у объекта у; и ну левое — если сравниваемые значения равны. Данный интерфейс можно использовать для указания способа сортировки элементов коллекции. В интерфейсе IEqualityComparer определяются два метода.