Thursday, February 12, 2009

Prefer IEnumerable<T> to IList<T> for Public Members

A coworker of mine knew vaguely of my preference for IEnumerable<T> over IList<T> for exposing collection members of a class. He needed to expose a collection and its count somehow, and asked me what I thought. Here it is:

I prefer IEnumerable<T> to IList<T> for the following reasons.

IEnumerable<T> is the lowest common denominator of collections, but still very useful as it is foreachable.

Because it is the lowest common denominator, my backing collection can be any number of specific collection types, including an array.

It is easy to return an empty, ad hoc, 'light-weight' enumeration: Enumerable.Empty<T>().

I can even not have a backing collection, and instead implement the enumeration in code using the yield keyword.

IList<T> has methods like Add() and Insert() and in most API situations I don't want clients adding members to my collection - IEnumerable<T> is immutable.

IEnumerable<T> is easily managed by Linq extension methods, e.g.

IEnumerable<IWidget> widgets = someObject.GetWidgets();
int count = widgets.Count();


Note that Count() is a method, not a property. It is an extension method (C# 3.0) that you have access to when you use System.Linq. The implementation, which you can see in Reflector, amounts to foreaching over the enumeration and incrementing a counter. It is efficient as it can be and we don't have to write it over and over.



It is simple to get an IEnumerable<T> into a List<T>:



List<IWidget> widgetList = new List<IWidget>();
widgetList.AddRange(widgets);



IEnumerable<T> is very composable - the new Linq extension methods allow you to do all sorts of magic with them - filtering, sorting, grouping, aggregation all chained together in a single line of code. IList<T> gets that too because it descends from IEnumerable<T>, but its pretty cool that the lowest common denominator collection gets this stuff.



The most persuasive argument is the one regarding clarity of the API. In every case I can think of, a collection property should present immutable interface, and IList<T> is mutable. If I thought I had a scenario where the client should be able to futz with my internal collection, I'd revisit my design because I probably have it wrong. IEnumerable<T> wins because it is clearly immutable.



There are alternatives that permit an IList<T> implementation that doesn't permit appending or inserting, but they're liars. For example List<T>.AsReadOnly() returns a ReadOnlyCollection, which implements IList<T>. But it lies. It has Add() and Insert() methods, but they always throw NotSupportedException. For me, it's a non-starter because the interface is misleading.



You could also just return a copy of your internal collection, something like this:



public IList<IWidget> Widgets
{
get
{
IList<IWidget> temp = new List<IWidget>();
temp.AddRange(_widgets);
return temp;
}
}


But I think this is just as misleading. Sure, you can honestly add and remove items, but it implies that it is the internal collection when it really isn't. Again, IEnumerable<T> wins because it makes the API clearer.



You may have a nagging concern that you are getting your Count by iterating through the enumerations rather than having and storing it somewhere. I wouldn't worry about it. It all the cases I can think of, this is trading cycles for clarity in the API. Clarity always trumps performance until we know or at least have a realistic expectation of a performance issue.

But even then, you needn't worry. Because wherever possible, these extension methods on IEnumerable<T> try cast it to a higher level collection to optimize the code. So the Count() extension method tries to cast the target to ICollection<T> so it can use the Count property. Only if it can't do that does it actually iterate through the enumerable to get the count. Check it out in Reflector.



IEnumerable<T> is a clear first class citizen with the addition of all those nifty new extension methods in System.Linq. And even without those, it should be the first choice for exposing an immutable collection.