Avoiding Multiple Enumerations
Often, when working with IEnumerable, I stumble upon a Resharper warning telling me that I iterated the IEnumerable multiple times. Usually, it's not a problem since I am Alt+Enter away from converting it to a list. But if the data represented by the enumerable is too big, a different approach is required.
For example. Let's look at a code that throws an exception if there are no items, extract the first item for some specific work, and then do some more work on all items:public static void DoWork(IEnumerable<int> input)
{
if(!input.Any())
{
throw new Exception("Input cannot be empty");
}
Console.WriteLine($"First item is: {input.First()}");
foreach(var i in input)
{
Console.WriteLine($"Working on input {i}");
}
}
This code iterates the enumerable three times. It's not optimal, especially when the enumerable opens a remote connection.
To fix this problem, we can iterate the input once and execute everything we need in this one loop:
public static void DoWork2(IEnumerable<int> input)
{
bool any = false;
bool first = true;
foreach(var i in input)
{
any = true;
if(first)
{
Console.WriteLine($"First item is: {i}");
first = false;
}
Console.WriteLine($"wokring on input {i}");
}
if(!any)
{
throw new Exception("Input cannot be empty");
}
}
This code is more efficient, but it's less readable.
Our third, and in my opinion the cleanest approach, is to extract some of the functionality to extension methods to hide the complication:
public static void DoWork3(IEnumerable<int> input)
{
var inputWithCallbacks = input.OnEmpty(() => throw new Exception("Input cannot be empty"))
.OnFirst(() => Console.WriteLine($"First item is: {i}"));
foreach(var i in inputWithCallbacks)
{
Console.WriteLine($"Working on input {i}");
}
}
As you can see, the code is much cleaner now. And as you will see in a moment, we will only iterate it in the foreach loop.
public static IEnumerable<T> OnEmpty(this IEnumerable<T> enumerable, Action action)
{
bool isEmpty = true;
foreach(var item in enumerable)
{
isEmpty = false;
yield return item;
}
if(isEmpty)
{
action();
}
}
public static IEnumerable<T> OnFirst(this IEnumerable<T> enumerable, Action<T> action)
{
bool isFirst = true;
foreach(var item in enumerable)
{
if(isFirst)
{
isFirst = false;
action(item);
}
yield return item;
}
}
There is still a problem with this code. We have to make sure that the enumerable is fully enumerated (for example, in the case of OnCount)
Comments
Post a Comment