The C# below is a very simple loop but I think it is two loops. A coworker of mine says that he thinks it is a single loop. Can you tell me if it is one loop or two loops? Can you also tell me how to read the IL and prove to my coworker that it is two loops?
var ints = new List<int> {1, 2, 3, 4};
foreach (var i in ints.Where(x => x != 2))
{
Console.WriteLine(i);
}
If it turns out that this is actually one loop that is cool to. I would still like to know how I can read the IL and see that it is only one loop.
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 137 (0x89)
.maxstack 3
.locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> ints,
[1] int32 i,
[2] class [mscorlib]System.Collections.Generic.List`1<int32> '<>g__initLocal0',
[3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0000,
[4] bool CS$4$0001)
IL_0000: nop
IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
IL_0006: stloc.2
IL_0007: ldloc.2
IL_0008: ldc.i4.1
IL_0009: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
IL_000e: nop
IL_000f: ldloc.2
IL_0010: ldc.i4.2
IL_0011: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
IL_0016: nop
IL_0017: ldloc.2
IL_0018: ldc.i4.3
IL_0019: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
IL_001e: nop
IL_001f: ldloc.2
IL_0020: ldc.i4.4
IL_0021: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
IL_0026: nop
IL_0027: ldloc.2
IL_0028: stloc.0
IL_0029: nop
IL_002a: ldloc.0
IL_002b: ldsfld class [mscorlib]System.Func`2<int32,bool> ConsoleApplication1.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_0030: brtrue.s IL_0045
IL_0032: ldnull
IL_0033: ldftn bool ConsoleApplication1.Program::'<Main>b__1'(int32)
IL_0039: newobj instance void class [mscorlib]System.Func`2<int32,bool>::.ctor(object,
native int)
IL_003e: stsfld class [mscorlib]System.Func`2<int32,bool> ConsoleApplication1.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_0043: br.s IL_0045
IL_0045: ldsfld class [mscorlib]System.Func`2<int32,bool> ConsoleApplication1.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_004a: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::Where<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>,
class [mscorlib]System.Func`2<!!0,bool>)
IL_004f: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0054: stloc.3
.try
{
IL_0055: br.s IL_0067
IL_0057: ldloc.3
IL_0058: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
IL_005d: stloc.1
IL_005e: nop
IL_005f: ldloc.1
IL_0060: call void [mscorlib]System.Console::WriteLine(int32)
IL_0065: nop
IL_0066: nop
IL_0067: ldloc.3
IL_0068: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_006d: stloc.s CS$4$0001
IL_006f: ldloc.s CS$4$0001
IL_0071: brtrue.s IL_0057
IL_0073: leave.s IL_0087
} // end .try
finally
{
IL_0075: ldloc.3
IL_0076: ldnull
IL_0077: ceq
IL_0079: stloc.s CS$4$0001
IL_007b: ldloc.s CS$4$0001
IL_007d: brtrue.s IL_0086
IL_007f: ldloc.3
IL_0080: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0085: nop
IL_0086: endfinally
} // end handler
IL_0087: nop
IL_0088: ret
} // end of method Program::Main
Compiler translates your code into try-finally block, first it calls
GetEnumerator
method on the source, (which is the iterator returned fromWhere)
then enters the try block.The first insruction:
Jumps to the
IL_0067
to callMoveNext
on the iterator and then loads the result ofMoveNext
into the local variable (as the weird name suggests (CS$4$0001) this is a compiler generated variable):This instruction checks if the result return from
MoveNext
istrue
and if it is jumps back to theIL_0057
Then the execution continues, the same operation is keep running until
MoveNext
returnsfalse
.So yes, there is one loop in the code.You can find more information about the
IL
instructions in the documentation.In addition to this the code before
try
block might seem confusing but it basically creates aFunc<int, bool>
delegate which is your lambda expression (x => x != 2
) then passes it toWhere
method.And loads the result of it into the 3. (actually it's fourth,3
is the index) local variable in this line:Which is an
IEnumerator<int>
as you can see in the parameter list.Then your loop uses that iterator.