IEnumerable's lazy evaluation

Defer execution of IEnumerable returning methods until iteration

Home DailyDrop

Daily Knowledge Drop

When executing a method which returns IEnumerable, the method body is not execute until the result is enumerated over. Invoking the method will not cause any of the method code to execute, including any code before the first yield in the body - not until enumeration.


IEnumerable

A quick summary of IEnumerable and yield usage - when a method is defined to have a return type of IEnumberable<T>, it can be invoked and the results iterated over:

// call the GetNumbers method in the foreach loop
foreach(var number in GetNumbers())
{
    Console.WriteLine(number);
}

public IEnumberable<int> GetNumbers()
{
    yield return 1;
    yield return 2;
    yield return 3;
    yield return 4;
    yield return 5;
}

The great benefit of IEnumerable though, comes with its usage in conjunction with the yield keyword - this is used inside the method to return a value and (temporarily) yield control to the calling iterator. Once the iteration body (Console.WriteLine in our example) is complete, control is then returned back to the method, which is executed until the next yield is encountered.

The output for the above would be:

1
2
3
4
5

Lazy evaluation

In the above example we saw how the GetNumbers method was called as part of the iterator (as part of the foreach) - but it is also possible to invoke the method, and store the returned IEnumerable for later execution:

// call the GetNumbers method 
IEnumerable<int> numbers = GetNumbers();

// do more processing

// iterate over the IEnumerable<int> variable
foreach (var number in numbers)
{
    Console.WriteLine(number);
}

This results in the same output as the previous example above.


Lazy execution

The interesting part about the lazy evaluation (and the reason for this post), is that when using lazy evaluation, the method body is not executed when the method is called, only when it's iterated over:

Consider the following method which returns IEnumerable<string>, but before it returns a value it will log which value is being returned:

IEnumerable<string> GetStringsWithLogging()
{
    Console.WriteLine("Executing iteration 1");
    yield return "Iteration 1";

    Console.WriteLine("Executing iteration 2");
    yield return "Iteration 2";

    Console.WriteLine("Executing iteration 3");
    yield return "Iteration 3";

    Console.WriteLine("Executing iteration 4");
    yield return "Iteration 4";

    Console.WriteLine("Executing iteration 5");
    yield return "Iteration 5";
}

The method is executed as follows:

Console.WriteLine($"Before '{nameof(GetStringsWithLogging)}' called");
var deferLogging = GetStringsWithLogging();
Console.WriteLine($"After '{nameof(GetStringsWithLogging)}' called");

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

The output of the above is:

Before 'GetStringsWithLogging' called
After 'GetStringsWithLogging' called
Executing iteration 1
Iteration 1
Executing iteration 2
Iteration 2
Executing iteration 3
Iteration 3
Executing iteration 4
Iteration 4
Executing iteration 5
Iteration 5

From the output one can see that the body of GetStringsWithLogging is not invoked when it is initially called - it is only when the deferLogging variable is iterated over with the foreach loop, that the body of GetStringsWithLogging is executed.


Notes

My initial gut assumption with an IEnumerable method was that the body of the method in question (GetStringsWithLogging here) would execute up until the first yield when called, no matter if lazy or not. However working through sample examples, and understanding how the code is lowered, the deferred execution makes more sense - and I am glad my initial assumptions were incorrect.

Having the ability to defer execution of the method allows for potentially long running processes which retrieve the results data (for example), to be deferred until/if actually needed (obviously all by design, I am sure) it very valuable. The IEnumerable instance can be passed around between methods, and only materialized when required - instead of passing around the (potentially larger in size) materialized data, when it might not even be needed.


References

C#: IEnumerable, yield return, and lazy evaluation


Daily Drop 124: 26-07-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net ienumerable lazy iteration