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.

2 comments:

Adam Tuliper said...

If an enumerating count operation is a concern, you can still return IList and have an IEnumerable as the return type. This will implement ICollection and get an optimized "Count" without enumeration.

Charles Crary said...

That's true, but if you look at the implementation of Enumerable.Count you'll see that it does exactly that test. If the IEnumerable is an ICollection, then it simply calls the Count property on the ICollection.

This is one of the reasons extension methods are so cool.

The only think ICollection brings to the table is the Count property. And since Enumerable.Count() will use it if it can, I still prefer IEnumerable.

Thanks,

Chip