C# – IEnumerable, IEnumerator, IEnumerable и IEnumerator

Ранее уже было показано, как использовать оператор foreach для удобного выполнения итерации по коллекции объектов, включая System.Array, ArrayList, List и т.п. Как это функционирует?

Каждая коллекция, которая должна работать с foreach, должна реализовать интерфейс IEnumerable или IEnumerable. Этот интерфейс используется foreach для получения объекта, знающего, как перечислить, или выполнить итерацию, по элементам коллекции.

Объект итератора, полученный от IEnumerable, должен реализовать интерфейс IEnumerator или IEnumerator. Обобщенные типы коллекций обычно реализуют IEnumerator, а объект перечислителя — IEnumerator. Интерфейс IEnumerable наследуется от IEnumerable, a IEnumerator — от IEnumerator. Это позволяет применять обобщенные коллекции там же, где используются необобщенные.

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

После того, как будет продемонстрировано, насколько легко создавать перечислители с помощью блоков итераторов, вы поймете, что реализовать IEnumerable и IEnumerator — пара пустяков.

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

Если хотите, можете пропустить эту часть и перейти к следующему разделу, посвященному итераторам. При желании освежить в памяти тему реализации перечислителей читайте все.

Интерфейс IEnumerable предусмотрен для того, чтобы клиенты имели четко определенный способ получения перечислителя для коллекции. Ниже показано определение интерфейсов IEnumerable и IEnumerable:

1
2
3
4
5
6
7
8
public interface IEnumerable<t> : IEnumerable
{
IEnumerator<t> GetEnumerator();
}
public interface IEnumerable
{
IEnumerator GetEnumerator ();
}

Поскольку оба интерфейса реализуют метод GetEnumerator с одной и той же сигнатурой перегрузки (вспомните, что тип возвращаемого значения при разрешении перегрузки не учитывается), любая коллекция, реализующая IEnumerable, должна явно реализовать метод GetEnumerator. Больше всего имеет смысл явно реализовать необобщенный метод IEnumerable.GetEnumerator.

Интерфейсы IEnumerator и IEnumerator выглядят следующим образом:

1
2
3
4
5
6
7
8
9
10
public interface IEnumerator<t> : IEnumerator, IDisposable
{
T Current { get; }
}
public interface IEnumerator
{
object Current { get;  }
bool MoveNext ();
void Reset ();
}

Эти два интерфейса реализуют член с одинаковой сигнатурой — в данном случае свойство Current. В случае реализации IEnumerator должен быть явно реализовано свойство IEnumerator .Current. К тому же обратите внимание, что IEnumerator реализует интерфейс IDisposable. Позже будет объясняться, чем это хорошо.

А теперь рассмотрим, как реализовать IEnumerable и IEnumerator для доморощенного типа коллекции. Хороший учитель всегда сначала показывает, как сделать что-то “трудным способом”, а только потом переходит к “легкому способу”. Такой прием очень полезен, поскольку позволяет понять, что происходит “за кулисами”. Когда вы понимаете, как работает внутренний механизм, то лучше подготовлены к тому, чтобы иметь дело с техническими нюансами “легкого способа”.

Рассмотрим пример реализации IEnumerable и IEnumerator “трудным способом” для доморощенной коллекции целых чисел. В примере будет показано, как реализовать обобщенные версии, поскольку это предполагает также реализацию необобщенных версий. Чтобы не загромождать пример, ICollection не реализуется, а все внимание сосредоточивается только на интерфейсах перечисления.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
public class MyColl<t> : IEnumerable<t>
{
public MyColl( T[] items )  {
this.items = items;
}
public IEnumerator<t> GetEnumerator ()  {
return new NestedEnumerator ( this ) ;
}
IEnumerator IEnumerable.GetEnumerator ()  {
return GetEnumerator();
}
// Определение перечислителя.
class NestedEnumerator : IEnumerator<t>
{
public NestedEnumerator ( MyColl<t> coll )  {
Monitor.Enter ( coll.items.SyncRoot );
this.index = -1 ;
this.coll = coll;
}
public T Current {
get { return current; }
}
object IEnumerator.Current {
get {
return Current; }
}
public bool MoveNextO  {
if ( ++index >= coll.items.Length )  {
return false;
} else {
current = coll.items[index];
return true;
}
public void Reset()  {
current = default(T);
index =0;
}
public void Dispose ()  {
try {
current = default(T);
index = coll.items.Length;
}
finally {
Monitor.Exit( coll.items.SyncRoot );
}
private MyColl<t> coll;
private Т current;
private int index;
}
private T[] items;
}
public class EntryPoint {
static void Main()  {
MyColl<int> integers =
new MyColl<int> ( new int[]  {1, 2, 3, 4} );
foreach ( int n in integers )  {
Console.WriteLine ( n );
}

В более реальных случаях пользовательский класс коллекции наследуется от Collection и реализация IEnumerable получается бесплатно.

В этом примере внутренний массив МуСоLL<Т> инициализируется случайным набором целых чисел, так что перечислитель получает некоторые тестовые данные. Разумеется, в реальном контейнере должен быть реализован интерфейс ICollection, что позволит динамически наполнять коллекцию элементами.

Оператор foreach разворачивается в код, получающий перечислитель вызовом метода GetEnumerator на интерфейсе IEnumerable. Компилятор достаточно интеллектуален, чтобы использовать в данном случае IEnumerator.GetEnumerator вместо IEnumerator.GetEnumerator.

После получения перечислителя запускается цикл, в котором сначала вызывается метод MoveNext, а затем переменная п инициализируется значением, возвращенным из свойства Current. Если цикл не содержит других путей выхода, он продолжается до тех пор, пока MoveNext не вернет false. В этот момент перечислитель завершает работу с элементами коллекции, и для его использования понадобится вызвать Reset на перечислителе.

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

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

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

Если коллекция изменяется в процессе ее перечисления, то перечислитель становится семантически некорректным, и последующее его использование может привести к неопределенному поведению. Если необходимо предохранить целостность в таких ситуациях, то может понадобиться, чтобы перечислитель блокировал коллекцию через объект, предоставляемый свойством SyncRoot.

Очевидно, что блокировка должна быть получена в конструкторе перечислителя. Кроме того, эту блокировку нужно где-то снимать. Уже известно, что для выполнения такой детерминированной очистки необходимо реализовать интерфейс IDisposable. Именно в этом состоит причина реализации интерфейса IDisposable в IEnumerator.

Более того, код, сгенерированный оператором foreach, создает “за кулисами” блок try/finally, который вызывает Dispose на перечислителе внутри блока finally. Описанная техника в действии продемонстрирована в предыдущем примере.

Типы, производящие коллекции

Ранее уже подчеркивалось, что содержимое коллекции может измениться, пока перечислитель проходит по коллекции. Если коллекция изменяется, это может сделать перечислитель недействительным.

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

Цикл foreach может выполнять какую-то длительную обработку каждого элемента, и при этом для кого-то другого коллекция окажется недоступной для модификаций.

В подобных случаях может иметь смысл, чтобы каждый цикл foreach выполнялся над копией коллекции вместо самой исходной коллекции. Если вы решите сделать это, то должны хорошо понимать, что означает копирование коллекции. Если коллекция содержит типы значений, то под копированием понимается создание “глубокой” копии, поскольку типы значений в коллекции не имеют внутри себя ссылочных типов.

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

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

Применение методов обычно указывает на то, что над типом выполняются некоторые операции, результат которых ожидается. С другой стороны, доступ через свойства указывает на то, что требуется напрямую получить доступ к внутреннему состоянию самого объекта.

Именно поэтому данное эмпирическое правило имеет хороший семантический смысл. Аналогичное семантическое разделение имеет смысл применять ко всем свойствам и методам внутри создаваемых типов.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100