IEnumerable vs IReadOnlyList



I apologize to everyone who’s waiting for my response to their code review requests. I was busy dealing with some personal stuff the last couple of weeks. Moving forward, I’m going to maintain a one post a week schedule where “regular” articles would take turns with code reviews. This article is a regular one, so the one next week would be a code review.

Today, I’d like to talk about IEnumerable and IReadOnlyList collection interfaces. We’ll discuss which of them is preferable and in what circumstance.

IEnumerable vs IReadOnlyList

I wrote about similar topics a while back (Return the most specific type, accept the most generic type and IEnumerable interface in .NET and LSP) but hey, that was 2 years ago, and no one read my blog back then. Also, the subject of IEnumerable versus IReadOnlyList was not the primary focus of those posts, and so I decided to reiterate on that. But I encourage you to go ahead and read those articles regardless.

When talking about collections in a method’s signature, the two most popular types programmers use to represent those collections are IEnumerable and IReadOnlyList. Why is that? Why not, say, IEnumerable and ICollection? Or ICollection and IReadOnlyList? And why not just use a concrete collection class, such as List or Dictionary?

The answer to the first question is that people like working with immutable data. Immutability is a big deal in the modern software development world, even if you don’t practice functional programming much. It’s much easier to trace the program flow when it leaves distinct marks during its execution. For example, it’s pretty straightforward to trace what the following code does:

IReadOnlyList<string> names = ExtractNames(data);

IReadOnlyList<string> removedBrackets = RemoveBrackets(names);

IReadOnlyList<string> garbageFiltered = FilterGarbage(removedBrackets);

IReadOnlyList<string> filtered = FilterIncorrectNames(garbageFiltered);

That’s because you have the results of each of the intermediate steps right before you. They are easy to validate and verify.

Now, compare it to something like this:

List<string> names = ExtractPredefinedNames(data);

RemoveBrackets(names);

FilterGarbage(names);

FilterIncorrectNames(names);

Not so much for traceability, huh? A bug could sneak in in any of the above steps and it’s now harder to get to know where exactly it took place because all the evidence has been eradicated by data mutation.

Of course, one could adhere to functional style and preserve immutability even when working with plain List. But in many cases, it’s easier to maintain the order when the programming language itself helps you with that by disallowing changing collections in the first place. And that’s where IEnumerable and IReadOnlyList are helpful. They are the only interfaces in .NET which allow you state your intent upfront and signalize the client of your code that a collection cannot be changed. That is true even if that client is yourself.

Before moving forward, I need to do a couple of remarks here. First, along with IReadOnlyList, there is also IReadOnlyCollection, so technically, there are three such interfaces, not two. There’s no practical reason for using this interface, however, because all collections in .NET implementing IReadOnlyCollection also implement IReadOnlyList. And as the latter is more functional in terms of available API, it makes sense to prefer that interface over IReadOnlyCollection.

Second, the sole fact that some method returns IReadOnlyList or IEnumerable doesn’t guarantee that the underlying collection is immutable. If the client knows what collection type is actually being returned, he can implement a hack like this:

IReadOnlyList<string> names = ExtractPredefinedNames(data);

var mutableNames = (List<string>)names;

and still mutate that collection. This, however, is a gross violation of a basic OOP principle. Which is: you should not assume anything more about an object than what its type is telling you. Got a read-only collection interface? Deal with it appropriately, or else have compatibility issues when the method’s author decides to change the type of the underlying collection.

Alright, so this is the first reason why programmers like to use IEnumerable and IReadOnlyList. It helps “seal” collections, mark them as unchangeable. The second reason is the Open-Closed Principle. Programming to an interface allows you to easily swap the underlying collections as long as they implement the same interface. All that without changing any client code.

So, which one should you prefer? There’s a good practice when it comes to designing your APIs (and I wrote about it here): be conservative in what you send, be liberal in what you accept, also known as Postel’s law.

For our purposes, the “be liberal” part means: accept the most generic type you can. Whereas “be conservative”: return the most specific type you can. So, to paraphrase: prefer IEnumerable when accepting a collection; prefer IReadOnlyList when returning one.

Note that IEnumerable is not the most generic collection type possible. The most generic type of all types (including collection types) in .NET is Object. So why not just accept an object instead of IEnumerable? This is for sure would be more liberal than IEnumerable, right?

Actually, no. We still need to adhere to this basic principle saying that our code should not assume anything about an object other than what its type says. In other words, no downcast. So, if an input parameter is of type object, it is incorrect to cast it down to IEnumerable. Better state the required type upfront, that’s why we have strong type systems.

That’s by the way also true for IEnumerable itself. This interface provides only the basic functionality for working with collections (really, just iterating through them), and so if your method requires more than that, also state that requirement up front. For example, if the only thing you need out of a collection is the capability to enumerate its elements, it’s fine to use IEnumerable:

public void SomeMethod(IEnumerable<string> collection)

{

    foreach (string element in collection)

    {

        /* work with element */

    }

}

But if you need to access them by the index or know how many of them are in the collection, use IReadOnlyList:

public void SomeMethod(IReadOnlyList<string> collection)

{

    int count = collection.Count;

           

    /* use count */

 

    string element = collection[10];

}

So, be as liberal with the input collection type as you can, but not more. If you can’t make it work with IEnumerable, use IReadOnlyList.

Similarly, if you can return the more specific collection type from a method, do that. Use IReadOnlyList when possible. Fall back to IEnumerable when not.

Note that for private methods, it’s fine to use concrete collection types, such as List or Dictionary. You need to adhere to OCP only when the method is part of a public API.

Following this simple principle helps the clients of your code be more productive. When passing collections to your methods, they are free to use any concrete collection type because all of them implement IEnumerable. And when working with the output collection, they have the fully functional API on top of it; they are not restricted to using only the bare minimum IEnumerable provides.

Summary

IEnumerable has been with us from the beginning of time. For many years, it was a de facto standard way to represent a read-only collection. Since .NET 4.5, however, there is another way to do that: IReadOnlyList. Both collection interfaces are useful. Prefer IEnumerable when accepting a collection. Prefer IReadOnlyList when returning one.

Related posts:

Share




  • Brian Hinchey

    When I have tried accepting IEnumerable for collection parameters, I often find this leads to a lot of deferred execution enumerable iterators being passed around unintentionally because developers (including myself) forget to add .ToArray() or .ToList() at the end of their LINQ statements.

    E.g.
    var largeOrders = orders.Where(o => o.Amount > 100); //Creates deferred execution enumerable iterator
    ProcessOrders(largeOrders); //Will fail at compile time if parameter is IReadOnlyList rather than IEnumerable

    This makes the code harder to debug and occasionally introduces unintended behaviour and/or bugs due the unpredictability of when and how often the enumerable iterator will be executed.

    Do you think there is some pragmatic value in using IReadOnlyList for collection parameters so that the compiler will help identify cases where developers are accidentally using an enumerable iterator? Enumerable iterators certainly have their specific uses, but if they are used everywhere I find they become a bit of a headache.

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Yes, definitely. You raise an important topic. Another great benefit of IReadOnlyList over IEnumerable (aside from richer API) is that it prevents accidental LSP violation related to lazy evaluation. The compiler indeed helps identify potential problematic areas in code.

      It’s important to note, though, that it’s not the responsibility of the method accepting an IEnumerable to catch those errors. By defining a collection of type IEnumerable, it expects that collection to be enumerable, and reasonably so. If the collection is not enumerable (due to DB connection closure), it’s a problem on the client side.

      But nonetheless, having IReadOnlyList helps track such problems, regardless of who’s fault they are.

      • Kerem İspirli

        I’m trying to understand what you meant in second paragraph. Do you mean that those Linq query objects are the ones which break LSP by adding a step of DB connection?

        Accepting an IEnumerable parameter into my method doesn’t tell anything about the collection’s availability; isn’t it that method’s responsibility to make sure the parameter passed to it satisfies such preconditions? I may intentionally defer execution of such a query till a later step, or I may just forget to execute the query and pass the query itself around as if it is the actual collection. If a method is only working with the items, isn’t it the method’s responsibility to enforce that only an actual, existing collection of items can be passed as its parameter?

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          Do you mean that those Linq query objects are the ones which break LSP by adding a step of DB connection?

          Yes. But only when the DB connection is closed at the moment of evaluating the query because that would lead to an exception.

          In order to decide whose responsibility it is to verify the connection availability, you need to look at the interface. The LINQ query implements IEnumerable which gives you access to only 3 members: MoveNext, Current, and Reset. None of these methods provide an interface for verifying that the DB connection is still open. If there were such a method, then it would be a responsibility of the accepting method to verify it.

          To be precise and speak in terms of Design by Contract: the implementing class (the linq query) has strengthened the preconditions of the interface (IEnumerable in our case). It requires the client code to do things that the original interface didn’t account for. Hence the LSP violation by the linq query object.

          • Kerem İspirli

            OK I got that part: LINQ violated LSP.

            What about my method which accepts an IEnumerable parameter, which doesn’t say anything about availability of the items? Since my method is going to break when the contents of the IEnumerable are not absolutely available, shouldn’t I enforce their existance by requiring some other interface than IEnumerable? Like IReadOnlyList?

            It looks to me like even though verifying the connection availability is LINQ’s responsibility, enforcing existance of the items in the collection is the method’s responsibility (especially since LINQ is not the only possible data source). When I think this way, I cannot think of a use case for accepting an IEnumerable parameter to my method – which is your recommendation when applicable.

            So, when is it applicable? What am I missing?

          • http://enterprisecraftsmanship.com/ Vladimir Khorikov

            What about my method which accepts an IEnumerable parameter, which doesn’t say anything about availability of the items?

            It does tell us about availability of the items. The existence of the members `Current` and `MoveNext` imply that there are some elements in the collection that we can get access to. The only difference between IEnumerable and IReadOnlyList is that the latter provides a richer API (Count, and accessing elements by index). In all other respects, they are the same. The fact that we treat IEnumerable as a less safe option comparing to IReadOnlyList is a result of .NET Framework breaking LSP all over the place. We’ve just got used to it.

            Now, I would recommend being pragmatic here. If in your codebase IEnumerables are mostly unsafe (due to situations I described previously: lazy evaluations upon a closed DB connection), then using IReadOnlyList would be a better option for you. But we need to be clear here: it’s not an IEnumerable’s fault.

  • Kerem İspirli

    In your other article (http://enterprisecraftsmanship.com/2015/01/29/ienumerable-interface-in-net-and-lsp/) you also mention the infinite sequence problem (which I haven’t encountered yet but I see it a valid point against using IEnumerable anyway) but you didn’t mention it in this article. What are your thoughts about it?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      It could very well be the case that the client code passes an infinite IEnumerable as an input parameter. That would be a problem and strictly speaking, you should not use IEnumerable in a situation which assumes that the collection is finite because IEnumerable doesn’t provide such guarantees. This is highly theoretical case, though, and I haven’t yet seen it in practice. So I think it’s safe so say that we can disregard it and treat IEnumerable collections as finite in vast majority of cases.