Another great question from the C# subreddit. Say you have a foreach loop that looks like the following. Does the function in the foreach iterator get executed each iteration, or is the result cached?

foreach(var item in GetItems())
{
    Console.WriteLine(item);
}

There’s two parts to this question as I learned earlier.

First – does the result in your foreach iterator get cached? The answer is YES. We’ll take a look at that in a minute.

Second – how is it cached? It depends on whether you’re dealing with an IEnumerable or an Array.

Let’s look at some sample code.

        private static void PrintFiles2()
        {
            foreach (var file in System.IO.Directory.GetFiles("."))
            {
                Console.WriteLine(file);
            }
        }

If you run this code, it’ll print out the files in the current working directory. What does the compiled version look like?

Arrays

In this case, GetFiles() returns string[]. The compiled version of this code stores the result of GetFiles on the stack. The compiler also gives us an int32 on the stack. This stores the current index of our loop. In other words, the foreach gets translated into a regular loop, and we store (cache) the value returned by GetFiles in a local variable. The compiler takes care of all this for us.

    // .locals init (
    // [0] string[] V_0,
      // [1] int32 V_1,
      // [2] string 'file [Range(Instruction(IL_0014 stloc.2)-Instruction(IL_0016 ldloc.2))]'
    // )
    // 
    // IL_0000: nop
    // IL_0001: nop
    //
    // Load the "." string onto the stack, then call GetFiles,
    // which uses the parameter at the top of the stack as
    // the directory to search.
    //
    // IL_0002: ldstr        "."
    // IL_0007: call         string[] [System.IO.FileSystem]System.IO.Directory::GetFiles(string)
    //
    // Store the result array in stack variable V_0.
    //
    // IL_000c: stloc.0      // V_0
    //
    // Initialize our index variable to 0 and store it.
    //
    // IL_000d: ldc.i4.0
    // IL_000e: stloc.1      // V_1
    //
    // Here's where things get a little weird. First we go to address
    // IL_0022, where we see if the length of the array is > 0.
    //
    // Go to IL_0022 now...
    //
    // IL_000f: br.s         IL_0022
    // // start of loop, entry point: IL_0022
    // IL_0011: ldloc.0      // V_0
      // 
      // Now we push our array onto the stack and load the first
      // item at index V_0 onto the stack.
      // 
      // IL_0012: ldloc.1      // V_1
      // IL_0013: ldelem.ref
      //
      // Take the last item read from the array and put it into
      // our local variable file.
      //
      // IL_0014: stloc.2      // 'file [Range(Instruction(IL_0014 stloc.2)-Instruction(IL_0016 ldloc.2))]'
      // IL_0015: nop
      //
      // Load that local variable file onto the stack and 
      // call Console.Writeline.
      //
      // IL_0016: ldloc.2      // 'file [Range(Instruction(IL_0014 stloc.2)-Instruction(IL_0016 ldloc.2))]'
      // IL_0017: call         void [System.Console]System.Console::WriteLine(string)
      // IL_001c: nop
      // IL_001d: nop
      //
      // Add 1 to V_1, our position in the array.
      //
      // IL_001e: ldloc.1      // V_1
      // IL_001f: ldc.i4.1
      // IL_0020: add
      // IL_0021: stloc.1      // V_1
      //
      // Load (push) the value from our index variable onto the stack.
      //
      // Then push our array onto the stack - then get the length of it.
      //
      // IL_0022: ldloc.1      // V_1
      // IL_0023: ldloc.0      // V_0
      // IL_0024: ldlen
      // IL_0025: conv.i4
      //
      // Now we can compare our index variable to the length of
      // the array. If V_1 is < V_0.Length, jump to IL_0011. Otherwise
      // we exit the loop. 
      //
      // IL_0026: blt.s        IL_0011
      // // end of loop
    // IL_0028: ret

IEnumerable<T>

What happens if we deal with a List<T> or something that implements IEnumerable<T>? I have two functions: GetItems which returns an IEnumerable<string> and PrintList, which foreach-es over the items returned by GetItems and prints them to console.

public static void PrintList()
{
    foreach(var item in GetItems())
    {
        Console.WriteLine(item);
    }
}

private static IEnumerable<string> GetItems()
{
    return new List<string>() { "One", "Two", "Three" };
}

In this case, the compiler will generate a local variable which is an Enumerator for the result returned by GetItems. Enumerators have a property called Current, which retrieves the current value, and the method MoveNext(), which returns true if there are more items in the enumerator.

    // .locals init (
    // [0] class [System.Runtime]System.Collections.Generic.IEnumerator`1<string> V_0,
      // [1] string 'str [Range(Instruction(IL_0015 stloc.1)-Instruction(IL_0017 ldloc.1))]'
    // )
    // 
    // IL_0000: nop
    // IL_0001: nop
    //
    // First, call GetItems. This puts a reference to the IEnumerable
    // on the stack.
    //
    // IL_0002: call         class [System.Runtime]System.Collections.Generic.IEnumerable`1<string> ConsoleApp5.Program::GetItems()
    //
    // Then get the IEnumerator<string> for the IEnumerable<string> that
    // is on our stack, and store that in V_0.
    //
    // IL_0007: callvirt     instance class [System.Runtime]System.Collections.Generic.IEnumerator`1<!0/*string*/> class [System.Runtime]System.Collections.Generic.IEnumerable`1<string>::GetEnumerator()
    // IL_000c: stloc.0      // V_0
    //
    // Where did this try come from? IEnumerators implement IDisposable,
    // so we have to call Dispose() later.
    //
    // .try
    // {
    //
    // First go to instruction IL_001f. Why? That's where we check
    // to see if there are more items in the enumerator. We didn't call
    // MoveNext() outside this loop, so we need to call it for the first
    // time.
    //
    // IL_000d: br.s         IL_001f
      // // start of loop, entry point: IL_001f
      //
      // Load the IEnumerator onto the stack, then get the 
      // current value and store that in our temporary stack
      // variable str.
      //
      // IL_000f: ldloc.0      // V_0
        // IL_0010: callvirt     instance !0/*string*/ class [System.Runtime]System.Collections.Generic.IEnumerator`1<string>::get_Current()
        // IL_0015: stloc.1      // 'str [Range(Instruction(IL_0015 stloc.1)-Instruction(IL_0017 ldloc.1))]'
        // IL_0016: nop
        //
        // Load our temporary string variable and print it to console.
        //
        // IL_0017: ldloc.1      // 'str [Range(Instruction(IL_0015 stloc.1)-Instruction(IL_0017 ldloc.1))]'
        // IL_0018: call         void [System.Console]System.Console::WriteLine(string)
        // IL_001d: nop
        // IL_001e: nop
        //
        // Load our enumerator onto the stack and call MoveNext. That
        // will put the result (true or false) onto the stack.
        //
        // IL_001f: ldloc.0      // V_0
        // IL_0020: callvirt     instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext()
        //
        // If MoveNext returns true, then there are more items. So 
        // go back to IL_000f.
        //
        // IL_0025: brtrue.s     IL_000f
        // // end of loop
      // IL_0027: leave.s      IL_0034
      // } // end of .try
    // finally
    // {
    // IL_0029: ldloc.0      // V_0
      // IL_002a: brfalse.s    IL_0033
      // IL_002c: ldloc.0      // V_0
      // IL_002d: callvirt     instance void [System.Runtime]System.IDisposable::Dispose()
      // IL_0032: nop
      // IL_0033: endfinally
      // } // end of finally
    // IL_0034: ret
    // 

Isn’t it neat how the C# compiler generates loops? It generates the check and move next code at the end, and on the first run of the loop, jumps control to that position.

Published by Elias

Elias Puurunen is a versatile entrepreneur and President of Northern HCI Solutions Inc., an IT consulting firm which has worked with Fortune 500 companies, governments, and startups. He has spoken at conferences in Canada and the United States and has been published around the world. Part of his work led to an agreement between the Canadian Government and Siemens Canada, creating jobs and investment into green infrastructure. His company's event management app, the Tractus Event Passport connects people at conferences, seminars and symposiums across Canada. Today he is a consultant and advisor to technology firms and government organizations. He lectures at the University of Waterloo on Coding for Policy Analysis for the School of Public Policy. He is the author of Beyond Passwords: Secure Your Business, a cyber-security book for small business owners.

Leave a comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: