What I really want to do is very much like...
List<int> list = new List<int>(
new int[6] { 0,1,2,3,4,5 } );
forFirst(int i in list)
{
Console.WriteLine("Do something clever with "+i);
}
forAllButLast(int i in theRest)
{
Console.WriteLine(i);
}
forLast(int i in theRest)
{
Console.WriteLine("Do something else clever with "+i);
}
But of course that doesn't work, so after reading a post on the development of the C# language to support lambda expressions I wondered how close I can get.
Reconstructing foreach
Before we can change how something works, we need to understand how it works and attempt to replicate it. Here is a very simple foreach
foreach (int i in list)
{
Console.WriteLine(i);
}
which compiles to the following IL, please note the highlighted sections
L_0000: nop
L_0001: ldc.i4.6
L_0002: newarr int32
L_0007: dup
L_0008: ldtoken valuetype {B2C01433-68A8-4EFA-ACCD-880F1F4FCBE2}/
__StaticArrayInitTypeSize=24 {B2C01433-68A8-4EFA-ACCD-880F1F4FCBE2}::$$method0x6000001-1
L_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::
InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
L_0012: newobj instance void [mscorlib]System.Collections.Generic.List`1::.ctor(
class [mscorlib]System.Collections.Generic.IEnumerable`1)
L_0017: stloc.0
L_0018: ldloc.0
L_0019: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/
Enumerator [mscorlib]System.Collections.Generic.List`1::GetEnumerator()
L_001e: box [mscorlib]System.Collections.Generic.List`1/Enumerator
L_0023: stloc.1
L_0024: br.s L_0036
L_0026: nop
L_0027: ldloc.1
L_0028: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1::
get_Current()
L_002d: stloc.2
L_002e: ldloc.2
L_002f: call void [mscorlib]System.Console::WriteLine(int32)
L_0034: nop
L_0035: nop
L_0036: ldloc.1
L_0037: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
L_003c: stloc.3
L_003d: ldloc.3
L_003e: brtrue.s L_0026
L_0040: ret
This matches the documentation on the MSDN page, that foreach is most likely to be equivalent to the following code.
IEnumerator<int> enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
int i = enumerator.Current;
Console.WriteLine(i);
}
And that compiles to the following IL
L_0000: nop
L_0001: ldc.i4.6
L_0002: newarr int32
L_0007: dup
L_0008: ldtoken valuetype {8373D3B1-DD7F-43BF-A82E-EDDB0C7487BB}/
__StaticArrayInitTypeSize=24 {8373D3B1-DD7F-43BF-A82E-EDDB0C7487BB}::$$method0x6000001-1
L_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::
InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
L_0012: newobj instance void [mscorlib]System.Collections.Generic.List`1::.ctor(
class [mscorlib]System.Collections.Generic.IEnumerable`1)
L_0017: stloc.0
L_0018: nop
L_0019: ldloc.0
L_001a: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/
Enumerator [mscorlib]System.Collections.Generic.List`1::GetEnumerator()
L_001f: stloc.2
L_0020: br.s L_0033
L_0022: ldloca.s CS$5$0000
L_0024: call instance !0 [mscorlib]System.Collections.Generic.List`1/
Enumerator::get_Current()
L_0029: stloc.1
L_002a: nop
L_002b: ldloc.1
L_002c: call void [mscorlib]System.Console::WriteLine(int32)
L_0031: nop
L_0032: nop
L_0033: ldloca.s CS$5$0000
L_0035: call instance bool [mscorlib]System.Collections.Generic.List`1/
Enumerator::MoveNext()
L_003a: stloc.3
L_003b: ldloc.3
L_003c: brtrue.s L_0022
L_003e: leave.s L_004f
L_0040: ldloca.s CS$5$0000
L_0042: constrained [mscorlib]System.Collections.Generic.List`1/Enumerator
L_0048: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_004d: nop
L_004e: endfinally
L_004f: nop
L_0050: ret
.try L_0020 to L_0040 finally handler L_0040 to L_004f
So not quite the same as I seem to be calling the methods defined in List instead of on the interface IEnumerator, but who knows that might actually be quicker as it may reduce the redirect of the virtual method.
Implementing the other for-s
Now we have an IEnumerator we have something that we can use to dive in and read at specific times. Getting the first is easy, just MoveNext() and read Current. However reading the last is harder, with a single direction of access (Enumerator only exposes MoveNext()) the obvious solution is to read all the list, and when I run out of things to read then the last thing I read has to be the last in the list. Scanning through like this would mean I would skip all the items I want between first and last, but it also gives us the opportunity to do something instead of just discard them. So now we can actually write.
| Desired |
Actual |
List<int> list = new List<int>(
new int[6] { 0,1,2,3,4,5 } );
forFirst(int i in list)
{
Console.WriteLine(
"Do something clever with " + i);
}
|
List<int> list = new List<int>(
new int[6] { 0,1,2,3,4,5 } );
IEnumerator enumerator = list.GetEnumerator();
if (enumerator.MoveNext())
{
using (int i = enumerator.Current)
{
Console.WriteLine(
"Do something clever with "+i);
}
} |
forAllButLast(int i in theRest)
{
Console.WriteLine(i);
} |
int previous;
if (enumerator.MoveNext())
{
previous = enumerator.Current;
while (enumerator.MoveNext())
{
using (int i = previous)
{
Console.WriteLine(i);
}
previous = enumerator.Current;
} |
forLast(int i in theRest)
{
Console.WriteLine(
"Do something else clever with "
+ i);
}
|
using (int i = previous)
{
Console.WriteLine(
"Do something else clever with "
+ i);
}} |
Unfortunately this still doesn't look like what we were trying to achieve, and for that I need to refer to an MSDN article I just can't find at the moment. Oh well, you'll just have to wait for Part II