Some people say "always use IList<T>
instead of List<T>
".
They want you to change your method signatures from void Foo(List<T> input)
to void Foo(IList<T> input)
.
These people are wrong.
It's more nuanced than that. If you are returning an IList<T>
as part of the public interface to your library, you leave yourself interesting options to perhaps make a custom list in the future. You may not ever need that option, but it's an argument. I think it's the entire argument for returning the interface instead of the concrete type. It's worth mentioning, but in this case it has a serious flaw.
As a minor counterargument, you may find every single caller needs a List<T>
anyway, and the calling code is littered with .ToList()
But far more importantly, if you are accepting an IList as a parameter you'd better be careful, because IList<T>
and List<T>
do not behave the same way. Despite the similarity in name, and despite sharing an interface they do not expose the same contract.
Suppose you have this method:
public Foo(List<int> a)
{
a.Add(someNumber);
}
A helpful colleague "refactors" the method to accept IList<int>
.
Your code is now broken, because int[]
implements IList<int>
, but is of fixed size. The contract for ICollection<T>
(the base of IList<T>
) requires the code that uses it to check the IsReadOnly
flag before attempting to add or remove items from the collection. The contract for List<T>
does not.
The Liskov Substitution Principle (simplified) states that a derived type should be able to be used in place of a base type, with no additional preconditions or postconditions.
This feels like it breaks the Liskov substitution principle.
int[] array = new[] {1, 2, 3};
IList<int> ilist = array;
ilist.Add(4); // throws System.NotSupportedException
ilist.Insert(0, 0); // throws System.NotSupportedException
ilist.Remove(3); // throws System.NotSupportedException
ilist.RemoveAt(0); // throws System.NotSupportedException
But it doesn't. The answer to this is that the example used IList<T>/ICollection<T> wrong. If you use an ICollection<T> you need to check the IsReadOnly flag.
if (!ilist.IsReadOnly)
{
ilist.Add(4);
ilist.Insert(0, 0);
ilist.Remove(3);
ilist.RemoveAt(0);
}
else
{
// what were you planning to do if you were given a read only list anyway?
}
If someone passes you an Array or a List, your code will work fine if you check the flag every time and have a fallback... But really; who does that? Don't you know in advance if your method needs a list that can take additional members; don't you specify that in the method signature? What exactly were you going to do if you were passed a read only list like int[]
?
You can substitute a List<T>
into code that uses IList<T>
/ICollection<T>
correctly. You cannot guarantee that you can substitute an IList<T>
/ICollection<T>
into code that uses List<T>
.
There's an appeal to the Single Responsibility Principle / Interface Segregation Principle in a lot of the arguments to use abstractions instead of concrete types - depend on the narrowest possible interface. In most cases, if you are using a List<T>
and you think you could use a narrower interface instead - why not IEnumerable<T>
? This is often a better fit if you don't need to add items. If you need to add to the collection, use the concrete type, List<T>
.
For me IList<T>
(and ICollection<T>
) is the worst part of the .NET framework. IsReadOnly
violates the principle of least surprise. A class, such as Array
, which never allows adding, inserting or removing items should not implement an interface with Add, Insert and Remove methods. (see also https://softwareengineering.stackexchange.com/questions/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties)
Is IList<T>
a good fit for your organisation? If a colleague asks you to change a method signature to use IList<T>
instead of List<T>
, ask them how they'd add an element to an IList<T>
. If they don't know about IsReadOnly
(and most people don't), then don't use IList<T>
. Ever.
Note that the IsReadOnly flag comes from ICollection<T>, and indicates whether items can be added or removed from the collection; but just to really confuse things, it does not indicate whether they can be replaced, which in the case of Arrays (which return IsReadOnlys == true) can be.
For more on IsReadOnly, see msdn definition of ICollection<T>.IsReadOnly