Home > dotnet > Differences between .NET Collection Interfaces

Differences between .NET Collection Interfaces

IList, ICollection, IReadOnlyList, and when to use them

by
Published: Last Updated on

If you’re anything like most new .NET developers you’ve probably seen IEnumerable<T>, IList<T>, and maybe even ICollection<T> around. If you’re anything like most people, you’ve probably found these interfaces confusing and been unsure which one to use when.

These interfaces are everywhere. For example, if you were to look at the List<T> class’s implementation in modern .NET you’d see it implements the following interfaces (among others):

  • IList<T>
  • ICollection<T>
  • IReadOnlyList<T>
  • IReadOnlyCollection<T>
  • IEnumerable<T>

Each one of these interfaces has a point, and each one is slightly different. However, many of these interfaces can be intimidating the first time you encounter them.

In this article we’ll explore these common .NET interfaces, what they provide, and when you might choose to use each one.

Note: There are also the older non-generic interfaces of IEnumerable and IList. For the purposes of this article we’ll focus instead on generic collection interfaces since these are more commonly used in modern .NET.

Why not just use List<T>?

When I teach my students about IEnumerable, I usually see eyes gloss over from the abstractness of the concept and the foreignness of the word enumerable. A modest chunk of my students start panicking and wonder if they can scurry back to the safety of just working with List<T> and pretend that interfaces aren’t there.

The short answer is this: You can ignore collection interfaces some of the time.

However, sometimes you have to work with these interfaces when others provide them to you, and sometimes it’s better to work with these interfaces instead of concrete types.

To explore this, let’s take a look at this simple example below:

private List<User> _users = new();
public List<User> Users
{
   get
   {
      return _users;
   }
}

Here it looks like we have a properly encapsulated List of User objects in the _users field. However, upon closer examination, there are a few problems:

First, by returning a List<User> we invite others to store the result of this call in List<User> variables or properties. This is fine for now, but if we ever change the type of collection we’re returning, our callers will need to update as well.

Secondly, by returning a List<User>, we give calling methods full access to the methods on that List object, including Add, Remove, and Sort. Worse, these methods will work and update the same List our code is using in its _users field.

This violates encapsulation and may result in inconsistent state within the class due to changes to the internal state of the class coming from outside of the class in unintended ways.

Alternatively, if this didn’t actually modify the class, but the caller still thought they could use the Add, Remove, or Sort methods, they are now confused and frustrated because our design didn’t indicate to them that they couldn’t do this.

While these problems aren’t typically catastrophic, it’d be better to avoid them if we can, which is where .NET collection interfaces come into play.

Common .NET Collection Interfaces

Because a picture can be far more efficient than entire sections, I’ve summarized these five interfaces in this comparison matrix:

In the sections below we’ll explore each one of these five interfaces and when to use each one.

IEnumerable<T>

IEnumerable<T> is a foundational interface in .NET that simply means that something can be looped over in a foreach statement.

IEnumerable<T> is supported by List<T>, arrays, and almost any other collection type in .NET.

That’s about all you need to know about IEnumerable<T>, but if you’d like a few more details, read on to learn about IEnumerator<T> as well.


IEnumerable<T> internally works by providing a GetEnumerator method that returns an IEnumerator<T> object.

This IEnumerator<T> provides ways to:

  • Get the current item (if one is present)
  • Move to the next item
  • Reset back to the first item

These three capabilities power any collection type that can be looped over.

ICollection<T>

ICollection<T> allows you to manage collections of items. All ICollection<T>s are also IEnumerable<T>.

Using ICollection<T> you can:

  • Get a Count of the number of items in a collection
  • See if a collection Contains something
  • Modify the collection by Add, Remove, and Clear methods
  • Copy the collection to an array
  • Determine if the collection is read only

Notably, ICollection<T> does not include array indexers in the interface, so you cannot use an indexer to get an item out of a collection by its index.

ICollection<T> is a great choice if you need to add and remove things from a collection as well as loop over it, but you don’t need to use an indexer.

IList<T>

IList<T> is an extremely powerful and common ordered collection interface in .NET.

IList<T>s all implement both ICollection<T> and IEnumerable<T> but add the ability to work with indexes.

Specifically, IList<T> adds the following capabilities:

  • A collection indexer that lets you use syntax like int number = myList[0];
  • Allows you to find the IndexOf an item in the list
  • Insert items at a specific index
  • RemoveAt a specific index

If your collection is ordered and has specific indexes and you want others to take full advantage of them, IList<T> is a great choice.

IReadOnlyCollection<T>

IReadOnlyCollection<T> is a rarer interface to see, but it is essentially a version of IEnumerable<T> that also has a Count of items.

You can also think about IReadOnlyCollection<T> as a version of Collection<T> that removes ways of modifying the items.

All IReadOnlyCollection<T>s are also IEnumerable<T>.

IReadOnlyCollection<T> is a good choice if you want to return a collection but don’t want to expose ways of manipulating it through the interface.

IReadOnlyList<T>

IReadOnlyList<T> is an extension of IReadOnlyCollection<T> but adds an indexer so that you can get values out of the collection by an index. Otherwise, IReadOnlyList<T> is functionally identical to IReadOnlyCollection<T>.

Securing against Casting

You may now be thinking that using an IReadOnlyList<T> or IReadOnlyCollection<T> protects you from others modifying your code.

However, that actually may not be fully true.

Let’s revisit our example from earlier, but take advantage of IReadOnlyList<T>:

private List<User> _users = new();
public IReadOnlyList<User> Users
{
   get
   {
      return _users;
   }
}

Here the class can use a full List<User> internally but only expose IReadOnlyList<User> to external callers.

This stops callers from doing the following:

// Wouldn't work since Users is an IReadOnlyList
myObject.Users.Add(new User("Matt Eland"));

However, since the actual object being returned above is still a List<User>, savvy callers may notice this and cast the result to a List<User> and then interact with it in a way you didn’t intend:

// Cast the result of the Users property to a List<User>
List<User> users = (List<User>)myObject.Users;

// This works
users. Add(new User("Matt Eland"));

Because of this, it’s a good idea to secure the items you intend to be read only by calling either ToList() or AsReadOnly() on them using LINQ to create a separate collection when returning values as shown below:

private List<User> _users = new();
public IReadOnlyList<User> Users
{
   get
   {
      return _users.AsReadOnly();
   }
}

Conclusion

While it’s certainly possible to write code that just returns Lists or other concrete types, using the appropriate .NET collection interface can help your code shine.

Using the right interface lets others see your intent, helps you properly encapsulate your classes, and gives you flexibility to change collection types in a class without impacting callers.

Author

  • Matt Eland

    After several decades as a software engineer and engineering manager, Matt now serves as a software engineering instructor at Tech Elevator where he gets to raise up future developers and unleash them upon the world to build awesome things. Matt is an Azure Data Scientist and AI Engineer Associate, runs a data science blog and YouTube channel, is currently pursuing a master's degree in data analytics, and helps organize the Central Ohio .NET Developer Group. In his copious amounts of spare time, Matt continues to build nerdy things and looks for ways to share them with the community.

Leave a Reply

Related Content

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More