In this article, we will learn about IEnumerable, ICollection, IList, and List. We will also focus on how to choose the right one.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Introduction to .NET Collection Interfaces

First, it’s essential to understand that there are two different versions of interfaces. One is a non-generic IEnumerable, ICollection, and IList interface and the other one is a type-safe generic IEnumerable<T>, ICollection<T>, and IList<T> interface.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

IEnumerable is the base of all the others. ICollection inherits IEnumerable. IList inherits the ICollection. And List is a class implementation of the IList.

In this article, we talk mainly about type-safe generic interfaces, notably in the examples. But the same applies to non-generic versions.

It’s also important to point out that the two versions don’t belong to the same namespace:

  • Non-Generic versions are in System.Collections
  • Generic versions are in System.Collections.Generic

What is IEnumerable<T> and When to Use It? 

The IEnumerable<T> interface is the base for all the collections.

When we want to iterate only, then IEnumerable<T> is the best choice. It is for looping through the collection in the forward direction only. It supports deferred execution and filtering. When we need a read-only operation, we can use IEnumerable<T>.

But we can’t iterate backward. We can’t perform operations at a particular position. We can’t access any item by its index. 

It has one method GetEnumerator(). It returns an IEnumerator<T> that can be used to iterate through the collection.

IEnumerator<T> provides methods like MoveNext(), Reset() and Current property.

Let’s understand this with an example:

public int CountSpecialCharacters(IEnumerable<char> specialCharacters)
{
    var count = 0;
    foreach(char c in specialCharacters)
    {
        count++;
    }

    return count;
}

We have a CountSpecialCharacters() method that takes specialCharacters of IEnumerable<char> type. To count the characters, we can use the for each loop which iterates over the specialCharacters.

The foreach uses GetEnumerator(), MoveNext() and Current to iterate over the collection hiding the complexity of Enumerators. That’s why we can use foreach with every type that implements IEnumerable<T> (that also means all the collection types we will tackle in this article).

There is another way to create IEnumerable<T> types (other than instantiating a concrete type like a List<T> for example) and that’s using the yield keyword. The yield keyword allows doing custom iteration while maintaining the state over a collection of items.

Let’s have an GetEvenNumberUpToTen() example:

public class ImplementationOfIEnumerable
{
    public IEnumerable<int> GetEvenNumberUpToTen()
    {
        yield return 0;
        yield return 2;
        yield return 4;
        yield return 6;
        yield return 8;
        yield return 10;
    }
}           

The caller Main() method calls GetEvenNumberUpToTen() to get even numbers.

var enu = new ImplementationOfIEnumerable();
Console.WriteLine($"IEnumerable With Yield:"); 
foreach (var num in enu.GetEvenNumberUpToTen())
{ 
    Console.WriteLine(num);
}

When the Main() caller calls GetEvenNumberUpToTen() then every time the control moves to the next yield and returns the value.

What is ICollection<T> and When to Use It? 

ICollection extends IEnumerable. It offers all the functionalities of the IEnumerable and adds a few new functionalities to it. ICollection allows to add or remove elements in the collection which is not possible with IEnumerable. But we still can’t perform any index-related operations. 

Let’s modify the above CountSpecialCharacters() method:

public int CountSpecialCharacters(ICollection<char> specialCharacters)
{
    specialCharacters.Add('~');
    specialCharacters.Add('!');

    var count = 0;
    foreach (char c in specialCharacters)
    {
        count++;
    }

    return count;
}

We have the same CountSpecialCharacters() method but this time specialCharacters is of ICollection<char> type. Also, instead of our iteration, we could use the Count property to show the number of elements inside the collection:

return specialCharacters.Count;

Now, not only can we loop through the characters but we can also Add() elements into the specialCharacters:

var icl = new ImplementationOfICollection();
ICollection<char> lst = new List<char>() { '^', '.' };

Assert.That(icl.CountSpecialCharacters(lst), Is.EqualTo(4));

This modified CountSpecialCharacters() method adds two more characters to the ICollection<char>. This returns the count as 4.

We can use ICollection when we need to perform any non-index related operations like Add(), Remove(), Contains(), CopyTo() etc.

What is IList<T> and When to Use It? 

IList extends ICollection. It exposes all the ICollection functionalities and also adds its operations to it. IList allows index-related operations like Insert(), RemoveAt() etc.

Let’s add more functionality to the same example:

public int CountSpecialCharacters(IList<char> specialCharacters)
{
    specialCharacters.Add('~');
    specialCharacters.Add('!');

    specialCharacters.Insert(0, '$');
   
    var count = 0;
    foreach (char c in specialCharacters)
    {
        count++;
    }

    return specialCharacters.IndexOf('$').Equals(0) ? count : 0;
}

We have the same CountSpecialCharacters() method but this time it has specialCharacters of IList<char> type. Now, not only can we loop through, we can Add() elements into the specialCharacters and also perform index-related operations Insert() or IndexOf() to a specific position in the IList.

We call the CountSpecialCharacters() from Main() method:

var specialCharacter = new List<char>() {'#','@','$' };
var arrayOfSpecialCharacter = new char[] { '=', '!' };
var ils = new ImplementationOfIList();
    
var output = ils.CountSpecialCharacters(specialCharacter);
Console.WriteLine($"IList with List input:{output}");
output = ils.CountSpecialCharacters(arrayOfSpecialCharacter);
Console.WriteLine($"IList with Array input:{output}");

We pass the specialCharacter  to the CountSpecialCharacters() which takes IList<char> type as input and returns the count. We may be tempted to pass an array to IList<T> parameter as we did in the example. but that can cause an issue since some methods  like Add(), Insert() etc are not implemented.

The compiler let us pass the array to the method because other functionalities are well-implemented and can be used properly.

In this example, on passing arrayOfSpecialCharacter of char[] type to CountSpecialCharacters() we get a runtime exception at Add() method: 

System.NotSupportedException: 'Collection was of a fixed size.'

What is List<T> and When to Use It? 

Till now we have seen all the interfaces, but List is a concrete class. List implements IList. List can be instantiated. We can use it whenever we want to have a generic list with a specific object type. List supports Sort() method. 

Let’s take the same CountSpecialCharacters() method:

public int CountSpecialCharacters(List<char> specialCharacters)
{
    var count = 0;
    foreach (char c in specialCharacters)
    {
        count++;
    }
    
    return count;
}

We change only the input type to List<char>. We have a List of char types in which we can use all the functionalities of the above three interfaces. We can pass the specialCharacter of List<char> type to the CountSpecialCharacters().

As List is a concrete class, when we expose a List instead of an interface, we couple the client to a specific implementation. We should choose the minimum required interface when exposing an API to different clients, in this way we allow flexibility to the client code and give them the choice to choose any implementation they want.

How to Choose the Right Collection Interface

We have learned about different interfaces and we should think about our requirements for choosing any one from the above. To choose the right one for the job, we should think about:

  • Do we need to iterate over a collection in read mode only, then let’s go for IEnumerable<T>
  • Do we have to manipulate the collection, then we can have ICollection<T>
  • Do we want to perform index-based operations over the collection, then we can choose IList<T>
  • When we want to have a concrete type to instantiate and work with, then we can pick List<T>

Considering all these points, we can have a well-chosen type for our specific use case.

Conclusion

That’s all for today and we hope that this article differentiates the characteristics and usage of these collection interfaces.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!