DisasterDev

Be careful with Enumerable

Published on Apr 10, 2022

dotnetc#programming

Do you use tools such as Rider or Resharper? If the answer is "no", then you probably should consider using them. If you do use any of them, you must have seen this warning very often.


Since it's just a warning, many developers will just choose to ignore it because let's face it, many real-world projects have hundreds of even thousands of warnings and they seem to be running "just fine". While we know some warnings are indeed over dramatic, it's not the best practice to just ignore them whatsoever; at least, we should understand why the warning was raised by the IDE.

The warning shown in the screenshot above is a classic one. Most people can get away with it because under the hood, the IEnumerable<T> is usually a concrete collection rather than a iterator.

Let me show you some buggy code thanks to ignoring the warning above. Let's consider we have BagService which returns a list of Bags and each Bag contains some "Stuff". Then we want to grab these bags and change the "Stuff" in each of the Bags to some "Better Stuff". So here's the code.

public class Program
{
    public static void Main(string[] args)
    {
        var service = new BagService();
        var bags = service.GetAllBags();
        foreach (var bag in bags)
        {
            bag.Stuff = $"Better {bag.Stuff}";
        }

        foreach (var bag in bags)
        {
            Console.WriteLine(bag.Stuff);
        }
    }

    public class BagService
    {
        public IEnumerable<Bag> GetAllBags()
        {
            for (var i = 0; i < 10; i++)
            {
                yield return new Bag($"Stuff {i}");
            }
        }
    }

    public class Bag
    {
        public string Stuff { get; set; }

        public Bag(string stuff)
        {
            Stuff = stuff;
        }
    }
}

If you run the code above, what output do you expect to see? Apparently the code was meant to produce output like this


Better Stuff 0
Better Stuff 1
...
Better Stuff 9

Unfortunately the actual output is


Stuff 0
Stuff 1
...
Stuff 9

This is because what's behind the function IEnumerable<Bag> GetAllBags() is an iterator produced by yield return. Such iterator does not give a concrete collection so every time you invoke it, you will get a new generated Bags. If you put a breakpoint at yield return new Bag($"Stuff {i}");, it will be hit 20 times during the whole program. To know more about iterators, read this.

This is why the IDE warns us about "multiple possible enumeration". It's not that you can't have enumerate multiple times, but most of the time, that is not what you want, and the IDE is unable to tell what the implementation of IEnumerable<Bag> is. Literally anything that can be enumerated has implemented this interface. For example, an array, a List<T>, a Collection<T>, a Set<T>, or an iterator, etc.

So how to avoid getting a new collection every time we invoke GetAllBags? Well, if we can't change the function itself to return a concrete collection, we can materialize its return value ourselves, such as:

public class Program
{
    public static void Main(string[] args)
    {
        var service = new BagService();
        // Use ToArray to materialize the iterator to an array
        var bags = service.GetAllBags().ToArray();
        foreach (var bag in bags)
        {
            bag.Stuff = $"Better {bag.Stuff}";
        }

        // Now we can be sure that
        // we're looping over the existing bags
        foreach (var bag in bags)
        {
            Console.WriteLine(bag.Stuff);
        }
    }
}

Now that we understand there could be a catch when consuming IEnumerable<T>, let's mention a little big about this interface. Personally, I always use it with caution. This is not to say that the interface itself is unsafe or not performant, but you definitely don't want to abuse it. When to use or not to use it is always case by case, but generally speaking, you can:


  • Use it as a function argument, so that you can be more generous to consumers.
  • Prefer to choosing a concrete collection over IEnumerable<T> as the return value of a function. In fact, when returning from a function, you can be as honest as possible. This is to reduce the cognitive burden for consumers. Of course, it is not to say you should always be as concrete as possible as there are times you want to give high level more abstractions which can leave you more room to change lower level logic in future.

When to return or accept what type is a large topic and I'm unable to cover it in an article of this length. If nothing else, what you can takeaway her is that you should not ignore those hints or warnings provided by your IDE; try to understand them and make sensible decisions about whether you should fix them or leave them.

© 2022 disasterdev.net. All rights reserved