Забыли пароль?

Foreach в C#

Dmitriy Fedyashov

Сегодння хотелось бы поговорить о том, как же работает цикл foreach внутри.

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

Цикл foreach рфботает по разному, в зависимости от коллекции, которую нужно перебирать.

1)      Если он имеет дело с банальным массивом, то мы всегда можем узнать его длину. Поэтому foreach в конечном итоге будет преобразован в цикл for. Вот, например:

1
2
3
4
5
int[] array = new int[]{1, 2, 3, 4, 5, 6};
foreach (int item in array)
{
 Console.WriteLine(item);
}

Компилятор преобразует цикл в такую конструкцию:

1
2
3
4
5
6
7
8
int[] temp;
int[] array = new int[]{1, 2, 3, 4, 5, 6};
temp = array;
for (int i = 0; i < temp.Length; i++)
{
 int item = temp[i];
 Console.WriteLine(item);
}

2)      Однако, многие коллекции не поддерживают индексированный доступ к элементам, например: Dictionary, Queue, Stack. В этом случае будет использован шаблон итератор.

Этот шаблон основан на интерфейсах System.Collections.Generic.IEnumerator <T> и nongeneric System.Collections.IEnumerator, которые позволяют осуществлять итерацию элементов в наборе.

IEnumerator содержит:

  • Метод MoveNext() – перемещает перечислитель на следующий элемент коллекции;
  • Метод Reset() – перезапускает перечисление, устанавливает перечислитель в начальную позицию;
  • Свойство Current – возвращает текущий элемент коллекции.

IEnumirator<T> наследуется от двух интерфейсов – IEnumirator и IDisposable. Он содержит перегрузку свойства Current предоставляя его реализацию по типу.

Раз уж мы упомянули интерфейс IDisposable, то уделим пару слов и ему. Он содержит единственный метод Dispose(), который нужен для освобождения ресурсов. Каждый раз, по завершении цикла или при выходе из него по другим причинам IEnumirator<T> очищает ресурсы.

Давайте посмотрим на такой цикл:

1
2
3
4
5
6
7
8
9
System.Collections.Generic.Queue<int> queue = new System.Collections.Generic.Queue<int>();
 queue.Enqueue(1);
 queue.Enqueue(2);
 queue.Enqueue(3);
 
 foreach (int item in queue)
 {
 Console.WriteLine(item);
 }

Компилятор преобразует его в подобный код:

1
2
3
4
5
6
7
8
9
10
11
System.Collections.Generic.Queue<int> queue = new System.Collections.Generic.Queue<int>();
 queue.Enqueue(1);
 queue.Enqueue(2);
 queue.Enqueue(3);
 
int num;
while (queue.MoveNext())
{
 num = queue.Current;
 Console.WriteLine(num);
}

В этом примере MoveNext заменяет необходимость подсчета элементов во время цикла. Когда он не получит очередной элемент, то вернет fasle и цикл завершится.

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

Он содержит единственный метод GetEnumerator(), который возвращает перечислитель. Таким образом IEnumerable<T> и его обобщенная версия IEnumerable позволяют вынести логику перечисления элементов из класса коллекции. Обычно это вложенный класс, который имеет доступ к элементам коллекции и поддерживает IEnumerator<T>. Имея каждый свой перечислитель, разные потребители не будут мешать друг другу, выполняя перечисление коллекции одновременно.

Таким образом, наш пример выше должен учитывать два момента – получение перечислителя и освобождение ресурсов. Вот как в действительности компилятор преобразует код цикла foreach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
System.Collections.Generic.Queue<int> queue = new System.Collections.Generic.Queue<int>();
 System.Collections.Generic.Queue<int>.Enumerator enumirator;
 
 IDisposable disposable;
 
 enumirator = queue.GetEnumerator();
 queue.Enqueue(1);
 queue.Enqueue(2);
 queue.Enqueue(3);
 
 try
 {
 int num;
 while (enumirator.MoveNext())
 {
 num = enumirator.Current;
 Console.WriteLine(num);
 }
 }
 finally
 {
 disposable = (IDisposable)enumirator;
 disposable.Dispose();
 }

Наверное, вы думаете, что для итерации коллекции необходимо реализовать интерфейсы IEnumerable и IEnumerable<T>. Однако это не совсем так. Для компиляции foreach вам достаточно, чтобы объект реализовывал метод GetEnumerator(), который вернет другой объект со свойством Current и методом MoveNext().

Здесь применяется утиная типизация – известный подход:

 «Если что-то ходит, как утка, и крякает, как утка, то это утка».

То есть, если есть объект с методом GetEnumerator(), который возвращает объект с методом MoveNext() и свойством Current, то это и есть перечислитель.

В противном случае, если нужные объекты, с нужными методами не найдены, будут искаться интерфейсы IEnumerable и IEnumerable<T>.

Таким образом foreach является действительно универсальным циклом, который отлично работает как с массивами, так и с коллекциями. Я использую его постоянно. Однако, несть один минус у foreach – он позволяет только читать элементы, и не позволяет их изменять. Поэтому старый добрый for никогда не пропадет из нашего кода.

Похожие статьи:

назад