DEV Community

André Slupik
André Slupik

Posted on

Why you probably shouldn't use IEnumerable<T>/seq<T>

What's wrong with this code?

// C#:
int Sum(IEnumerable<int> items)
{
    int total = 0;
    foreach(var item in items)
    {
        total += item;
    }
    return total;
}
Enter fullscreen mode Exit fullscreen mode
// F#
let sum (items: seq<int>) =
    let mutable total = 0
    for item in items do
        total <- total + item
    total
Enter fullscreen mode Exit fullscreen mode

If you said, "calling this function may hang forever", you'd be right. Here's a valid value for IEnumerable<int> (seq<int> in F#) that we could pass into our Sum function:

// C#
IEnumerable<int> GenerateValues()
{
    while (true)
    {
        yield return 1;
    }
}
Enter fullscreen mode Exit fullscreen mode
// F#
let generateValues () = 
    Seq.initInfinite id
Enter fullscreen mode Exit fullscreen mode

Now, Sum(GenerateValues()) hangs forever.

What's wrong with this code?

// C#
interface IConfigValuesProvider
{
    IEnumerable<ConfigValue> ConfigValues { get; }
}

var values = myConfigValueProvider.ConfigValues;
if (values.Count() > 0)
{
    foreach(var v in values) { ... }
}
Enter fullscreen mode Exit fullscreen mode
// F#
type IConfigValuesProvider =
    abstract ConfigValues : seq<ConfigValues>

let values = myConfigValueProvider.ConfigValues
if Seq.length values > 0 then
    for v in values do
        ...
Enter fullscreen mode Exit fullscreen mode

Well, nothing, probably, but you can't really be sure. Since ConfigValues is an IEnumerable<T>, it could technically be infinite, causing Count/Seq.length to hang, although, granted, that's unlikely. What sounds more plausible is that ConfigValues is implemented as the output of calls to Where or Select, and that iterating it is computationally expensive. Note that we're potentially causing it to be iterated twice here (calling Count and then doing a foreach), which would be bad if it was computationally expensive. Of course, the caller expects this to behave like an Array-like collection, but as a matter of fact, IEnumerable<T> makes no such promises. A potential solution here would be calling ConfigValue.ToArray() and using the result of that, but that's going to create a pointless copy if ConfigValues is actually just a collection. We can't know, so either we make unsafe assumptions, or we code defensively and waste resources.

IEnumerable<T>/seq<T> does not represent a collection of items, so it should not be used to represent collections of items.

So what should we use? Is there another interface that all collections implement and provide a guarantee that it's actually a collection? Yes, there is, and it's called IReadOnlyCollection<T>, introduced in .NET 4.5, August 15th, 2012. If you want to be more specific and get true array-like behavior (with indexing support), IReadOnlyList<T> has got your back. I'm leaning towards IReadOnlyList<T> for everything that has an index (arrays, List<T>, ImmutableArray<T>, etc.) and IReadOnlyCollection<T> for everything else (LinkedList<T>, Queue<T>, Stack<T>, etc.) There might be cases where concrete immutable collections like ImmutableArray<T> are better though, especially as a return type. Returning a concrete type seems to provide greater value. I haven't found great, clear guidance on e.g. IReadOnlyList<T> vs ImmutableArray<T>; if you have an opinion on that, please share!

Finally, if IEnumerable<T> does not represent collections, and we have a perfect replacement for it, what use does it have?

IEnumerable<T> is anything that supports enumeration: collections, infinite sequences, computations of successive values. It is the perfect type to define mappings over arbitrary, non-finite sequences of data. Think of LINQ operators Where, Select, which also return IEnumerable<T>: those make perfect sense and could not use a better type. Do not think of scalar-returning functions like Enumerable.Sum or Enumerable.Count which IMO shouldn't exist, as they don't make sense (and won't work) over anything but finite collections.

I suspect that the late addition of IReadOnlyCollection<T> and friends to .NET is largely to blame for the widespread use of IEnumerable<T> as a read-only view of a collection. After all, IEnumerable<T> arrived in .NET 2, early 2006, and System.Linq arrived in .NET 3.5, late 2007. F#'s Seq module, probably designed around the same time, is full of the same issues as System.Linq: Seq.sum, Seq.length, Seq.append, Seq.toList, Seq.toArray, etc. are all very practical, but definitely not sound.

Discussion (0)