Foreach в C sharp

06.10.2018

Сегодння хотелось бы поговорить о том, как же работает цикл 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 и nongeneric System.Collections.IEnumerator, которые позволяют осуществлять итерацию элементов в наборе.

IEnumerator содержит:

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

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

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

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

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 и его обобщенная версия IEnumerable позволяют вынести логику перечисления элементов из класса коллекции. Обычно это вложенный класс, который имеет доступ к элементам коллекции и поддерживает IEnumerator. Имея каждый свой перечислитель, разные потребители не будут мешать друг другу, выполняя перечисление коллекции одновременно.

Таким образом, наш пример выше должен учитывать два момента - получение перечислителя и освобождение ресурсов. Вот как в действительности компилятор преобразует код цикла 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. Однако это не совсем так. Для компиляции foreach вам достаточно, чтобы объект реализовывал метод GetEnumerator(), который вернет другой объект со свойством Current и методом MoveNext().

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

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

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

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

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

2 сентября 2024

Обзор облачного решения для создания и управления отчетами

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

Как собрать и установить плагин Postgres в FastReport .NET

В этой статье описывается подключение к базе посредством плагина FastReport .NET для дизайнера отчетов из Visual Studio через NuGet-сервер.
8 августа 2024

Как установить FastReport .NET и его компоненты в Windows

Пошаговая инструкция по онлайн и ручной установке через регистрационный код FastReport .NET и его компонентов в Windows.