I've implemented a foreach-loop and a while-loop that should create pretty much the same IL code.
The IL code (generated with compiler version 12.0.40629 for C#5) indeed is almost identical (with the natural exception of some numbers and so), but decompiler were able to reproduce the initial code.
What's the key difference that allows a decompiler to tell that the former code block is a foreach-loop while the latter one represents a while-loop?
The decompiled code that I provide below is generated with the latest version (as of today) of ILSpy (2.3.1.1855), but I also used JustDecompile, .NET Reflector, and dotPeek — with no difference. I didn't configure anything, I just the tools as they are installed.
Original code:
using System;
using System.Collections.Generic;
namespace ForeachVersusWhile
{
public class Program
{
public static void Main(string[] args)
{
var x = new List<int> {1, 2};
foreach (var item in x)
{
Console.WriteLine(item);
}
using (var enumerator = x.GetEnumerator())
{
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
}
}
}
Decompiled code:
List<int> x = new List<int>
{
1,
2
};
foreach (int item in x)
{
Console.WriteLine(item);
}
using (List<int>.Enumerator enumerator = x.GetEnumerator())
{
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
IL Code (loops only):
[...]
IL_0016: ldloc.0
IL_0017: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
IL_001c: stloc.s CS$5$0000
.try
{
IL_001e: br.s IL_002e
// loop start (head: IL_002e)
IL_0020: ldloca.s CS$5$0000
IL_0022: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_0027: stloc.1
IL_0028: ldloc.1
IL_0029: call void [mscorlib]System.Console::WriteLine(int32)
IL_002e: ldloca.s CS$5$0000
IL_0030: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0035: brtrue.s IL_0020
// end loop
IL_0037: leave.s IL_0047
} // end .try
finally
{
IL_0039: ldloca.s CS$5$0000
IL_003b: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0041: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0046: endfinally
} // end handler
IL_0047: ldloc.0
IL_0048: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
IL_004d: stloc.2
.try
{
IL_004e: br.s IL_005c
// loop start (head: IL_005c)
IL_0050: ldloca.s enumerator
IL_0052: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_0057: call void [mscorlib]System.Console::WriteLine(int32)
IL_005c: ldloca.s enumerator
IL_005e: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0063: brtrue.s IL_0050
// end loop
IL_0065: leave.s IL_0075
} // end .try
finally
{
IL_0067: ldloca.s enumerator
IL_0069: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_006f: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0074: endfinally
} // end handler
Background to the question:
I've read an article where they took a look at what C# code gets compiled to. In the first step they looked at a simple example: the foreach-loop.
Backed up by MSDN, a foreach loop is supposed to "hide the complexity of the enumerators". IL code doesn't know anything of a foreach-loop. So, my understanding is that, under the hood, the IL code of a foreach-loop equals a while-loop using IEnumerator.MoveNext.
Because the IL code doesn't represent a foreach-loop, a decompiler can hardly tell that a foreach-loop was used. That rose a couple of questions where people wondered why they saw a while-loop when they decompiled their own code. Here's one example.
I wanted to see that myself and wrote a small program with a foreach-loop and compiled it. Then I used a Decompiler to see what the code looks like. I wasn't expecting a foreach-loop, but was surprised when I actually got one.
The pure IL code, naturally, contained calls of IEnumerator.MoveNext etc.
I suppose I'm doing something wrong and hence enabling tools to access more information and, in consequence, correctly telling that I were using a foreach-loop. So, why am I seeing a foreach-loop instead of a while-loop using IEnumerator.MoveNext?