How is foreach implemented in C#? [duplicate]
-
16-06-2021 - |
Pregunta
How exactly is foreach
implemented in C#?
I imagine a part of it looking like:
var enumerator = TInput.GetEnumerator();
while(enumerator.MoveNext())
{
// do some stuff here
}
However I'm unsure what's really going on. What methodology is used for returning enumerator.Current
for each cycle? Does it return [for each cycle] or does it take an anonymous function or something to execute the body of foreach
?
Solución
It doesn't use an anonymous function, no. Basically the compiler converts the code into something broadly equivalent to the while loop you've shown here.
foreach
isn't a function call - it's built-into the language itself, just like for
loops and while
loops. There's no need for it to return anything or "take" a function of any kind.
Note that foreach
has a few interesting wrinkles:
- When iterating over an array (known at compile-time) the compiler can use a loop counter and compare with the length of the array instead of using an
IEnumerator
foreach
will dispose of the iterator at the end; that's simple forIEnumerator<T>
which extendsIDisposable
, but asIEnumerator
doesn't, the compiler inserts a check to test at execution time whether the iterator implementsIDisposable
- You can iterate over types which don't implement
IEnumerable
orIEnumerable<T>
, so long as you have an applicableGetEnumerator()
method which returns a type with suitableCurrent
andMoveNext()
members. As noted in comments, a type can also implementIEnumerable
orIEnumerable<T>
explicitly, but have a publicGetEnumerator()
method which returns a type other thanIEnumerator
/IEnumerator<T>
. SeeList<T>.GetEnumerator()
for an example - this avoids creating a reference type object unnecessarily in many cases.
See section 8.8.4 of the C# 4 spec for more information.
Otros consejos
Surprised the exact implementation is not touched. While what you have posted in the question is the simplest form, the complete implementation (including enumerator disposal, casting etc) is in the 8.8.4 section of the spec.
Now there are 2 scenarios where a foreach
loop can be run on a type:
If the type has a public/non-static/non-generic/parameterless method named
GetEnumerator
which returns something that has a publicMoveNext
method and a publicCurrent
property. As noted by Mr Eric Lippert in this blog article, this was designed so as to accommodate pre generic era for both type safety and boxing related performance issues in case of value types. Note that this a case of duck typing. For instance this works:class Test { public SomethingEnumerator GetEnumerator() { } } class SomethingEnumerator { public Something Current //could return anything { get { return ... } } public bool MoveNext() { } } //now you can call foreach (Something thing in new Test()) //type safe { }
This is then translated by the compiler to:
E enumerator = (collection).GetEnumerator(); try { ElementType element; //pre C# 5 while (enumerator.MoveNext()) { ElementType element; //post C# 5 element = (ElementType)enumerator.Current; statement; } } finally { IDisposable disposable = enumerator as System.IDisposable; if (disposable != null) disposable.Dispose(); }
If the type implements
IEnumerable
where theGetEnumerator
returnsIEnumerator
that has a publicMoveNext
method and a publicCurrent
property. But an interesting sub case is that even if you implementIEnumerable
explicitly (ie no publicGetEnumerator
method onTest
class), you can have aforeach
.class Test : IEnumerable { IEnumerator IEnumerable.GetEnumerator() { } }
This is because in this case
foreach
is implemented as (provided there is no other publicGetEnumerator
method in the class):IEnumerator enumerator = ((IEnumerable)(collection)).GetEnumerator(); try { ElementType element; //pre C# 5 while (enumerator.MoveNext()) { ElementType element; //post C# 5 element = (ElementType)enumerator.Current; statement; } } finally { IDisposable disposable = enumerator as System.IDisposable; if (disposable != null) disposable.Dispose(); }
If the type implements
IEnumerable<T>
explicitly then theforeach
is converted to (provided there is no other publicGetEnumerator
method in the class):IEnumerator<T> enumerator = ((IEnumerable<T>)(collection)).GetEnumerator(); try { ElementType element; //pre C# 5 while (enumerator.MoveNext()) { ElementType element; //post C# 5 element = (ElementType)enumerator.Current; //Current is `T` which is cast statement; } } finally { enumerator.Dispose(); //Enumerator<T> implements IDisposable }
Few interesting things to note are:
In both the above cases the
Enumerator
class should have a publicMoveNext
method and a publicCurrent
property. In other words, if you're implementingIEnumerator
interface it has to be implemented implicitly. For eg,foreach
wont work for this enumerator:public class MyEnumerator : IEnumerator { void IEnumerator.Reset() { throw new NotImplementedException(); } object IEnumerator.Current { get { throw new NotImplementedException(); } } bool IEnumerator.MoveNext() { throw new NotImplementedException(); } }
(Thanks Roy Namir for pointing this out.
foreach
implementation isnt as easy it seems on the surface)Enumerator precedence - It goes like if you have a
public GetEnumerator
method, then that is the default choice offoreach
irrespective of who is implementing it. For example:class Test : IEnumerable<int> { public SomethingEnumerator GetEnumerator() { //this one is called } IEnumerator<int> IEnumerable<int>.GetEnumerator() { } }
If you don't have a public implementation (ie only explicit implementation), then precedence goes like
IEnumerator<T>
>IEnumerator
.There is a cast operator involved in the implementation of
foreach
where the collection element is cast back to the type (specified in theforeach
loop itself). Which means even if you had written theSomethingEnumerator
like this:class SomethingEnumerator { public object Current //returns object this time { get { return ... } } public bool MoveNext() { } }
You could write:
foreach (Something thing in new Test()) { }
Because
Something
is type compatible withobject
, going by C# rules ,or in other words, the compiler lets it if there is an explicit cast possible between the two types. Otherwise the compiler prevents it. The actual cast is performed at run time which may or may not fail.