Why you probably shouldn't use IEnumerable<T>/seq<T>
Tags: dotnet, csharp, fsharp
Published: 2022-03-03 23:01:13

What's wrong with this code?

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

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:

```csharp
// C#
IEnumerable<int> GenerateValues()
{
    while (true)
    {
        yield return 1;
    }
}
```
```fsharp
// F#
let generateValues () = 
    Seq.initInfinite id
```

Now, `Sum(GenerateValues())` hangs forever.

What's wrong with this code?

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

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

let values = myConfigValueProvider.ConfigValues
if Seq.length values > 0 then
    for v in values do
        ...
```

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.