Search code examples
c#performancelazy-evaluationyield-return

Return results from a call to Yield Return


I often find the scenario where I return an IEnumerable using the yield return statement, then have other methods which call this function with different parameters and directly return the result.

Is there any performance benefit to iterating through the results end yield returning these as opposed to simply returning the collection? i.e. if I use return on the resulting IEnumberable rather than looping through these results again and using yield return does the compiler know to only generate the results as they're required, or does it wait for the entire collection to be returned before returning all results?

public class Demo
{
    private IEnumerable<int> GetNumbers(int x)
    {
        //imagine this operation were more expensive than this demo version
        //e.g. calling a web service on each iteration.
        for (int i = 0; i < x; i++)
        {
            yield return i;
        }
    }
    //does this wait for the full list before returning
    public IEnumerable<int> GetNumbersWrapped()
    {
        return GetNumbers(10);
    }
    //whilst this allows the benefits of Yield Return to persist?
    public IEnumerable<int> GetNumbersWrappedYield()
    {
        foreach (int i in GetNumbers(10))
            yield return i;
    }
}

Solution

  • GetNumbersWrapped simply passes through the original enumerable - it hasn't even invoked the iterator at that point. The end result remains fully deferred / lazy / spooling / whatever else.

    GetNumbersWrappedYield adds an extra layer of abstraction - so now, every call to MoveNext has to do ever so slightly more work; not enough to cause pain. It too will be fully deferred / lazy / spooling / whatever else - but adds some minor overheads. Usually these minor overheads are justified by the fact that you are adding some value such as filtering or additional processing; but not really in this example.

    Note: one thing that GetNumbersWrappedYield does do is prevent abuse by callers. For example, if GetNumbers was:

    private IEnumerable<int> GetNumbers(int x)
    {
        return myPrivateSecretList;
    }
    

    then callers of GetNumbersWrapped can abuse this:

    IList list = (IList)GetNumbers(4);
    list.Clear();
    list.Add(123); // mwahahaahahahah
    

    However, callers of GetNumbersWrappedYield cannot do this... at least, not quite as easily. They could, of course, still use reflection to pull the iterator apart, obtain the wrapped inner reference, and cast that.

    This, however, is not usually a genuine concern.