Published on Apr 10, 2022
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:
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