Today I would like to talk about the way the foreach loop works inside.
We all know that a foreach loop is - a loop that iterates through all the elements of the collection. Its greatest advantage in the ease of use - we do not need to worry about how many elements in the collection. However, many do not know that this is just syntactic “sugar”, which facilitates the work of the programmer. Therefore, we simply have to know in what the resulting compiler will convert.
The foreach loop works differently, depending on the collection you want to sort through.
1) If it has to deal with the banal array, we can always know its length. Therefore, foreach will eventually be converted to a for loop. For example:
1 2 3 4 5 |
int[] array = new int[]{1, 2, 3, 4, 5, 6}; foreach (int item in array) { Console.WriteLine(item); } |
The compiler converts the loop into this construct:
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) However, many collections do not support indexed access to elements, for example: Dictionary, Queue, Stack. In this case, the iterator template will be used.
This template is based on the interfaces System.Collections.Generic.IEnumerator <T> and nongeneric System.Collections.IEnumerator, which allow you to iterate the elements in the set.
The IEnumerator contains:
IEnumirator <T> is inherited from two interfaces - IEnumirator and IDisposable. It contains an overload of the Current property, providing its implementation by type.
Since we mentioned the interface IDisposable, then we'll tell a couple of words about it. It contains the only Dispose () method that is needed to free resources. Every time the loop terminates or when it exits, IEnumirator <T> clears the resources.
Let's look at this loop:
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); } |
The compiler converts it into a similar code:
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); } |
In this example, MoveNext replaces the need to count elements during the loop. When it does not receive the next element, it returns fasle and the loop terminates.
But, nevertheless, this code is only approximate to what the compiler really produces. The problem is that if you have two or more overlapping cycles that work with the same collection, then each MoveNext call will affect all the cycles. This course of events will not suit anyone. And so came up with the second interface IEnumirator.
It contains the only method GetEnumerator (), which returns an enumerator. Thus IEnumerable <T> and its generic version of IEnumerable allow you to render the logic of enumerating elements from the collection class. Usually this is a nested class that has access to the collection's elements and supports IEnumerator <T>. Having each enumerator, different consumers will not interfere with each other, performing the enumeration of the collection at the same time.
Thus, the above example should take into account two points - obtaining an enumerator and releasing resources. Here's how the compiler actually translates the foreach loop code:
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(); } |
You probably think that for the iteration of the collection, you need to implement the IEnumerable and IEnumerable <T> interfaces. However, this is not quite true. To compile foreach, you just need to implement the GetEnumerator () method, which will return another object with the Current property and the MoveNext () method.
Here we use duck typing - a well-known approach:
"If something goes like a duck, and quacks like a duck, it's a duck."
That is, if there is an object with the GetEnumerator () method, which returns an object with the MoveNext () method and the Current property, then this is the enumerator.
Otherwise, if the necessary objects, with the necessary methods are not found, the interfaces IEnumerable and IEnumerable <T> will be searched.
Thus foreach is really a universal loop that works fine with both arrays and collections. I use it constantly. However, there is one disadvantage for foreach - it allows only to read the elements, and does not allow them to be changed. Therefore, the old good “for” never will be lost from our code.