In this article, we will learn about IEnumerable, ICollection, IList, and List. We will also focus on how to choose the right one.
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.
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.