Сегодння хотелось бы поговорить о том, как же работает цикл 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 содержит:
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 никогда не пропадет из нашего кода.