Context: I sent an email to my colleagues telling them about Enumerable.Empty<T>()
as a way to return empty collections without doing something like return new List<T>();
I got a reply saying that the downside is that it doesn't expose a specific type:
That does present a tiny issue. It’s always good to be as specific as possible about your return type* (so if you’re returning a List<>, make that your return type); that’s because things like Lists and Arrays have extra methods that are useful. Plus it also can be useful in making performance considerations when using the collection from the calling method.
The trick below unfortunately forces you to return an IEnumerable, which is about as non-specific as possible, right? :(
* This is actually from the .NET Design Guidelines. Stated reasons in the guidelines are the same as I’m mentioning here, I believe.
This seemed to be the complete opposite of what I had learned, and try as I might, I couldn't find this exact advice in the design guidelines. I did find one small piece like this:
DO return a subclass of
Collection<T>
orReadOnlyConnection<T>
from very commonly used methods and properties.
With a code snippet following, but no more justification at all.
So that being said, is this a real and accepted guideline (the way it was described in the first block quote)? Or has it been misinterpreted? All other SO questions I could find have answers preferring IEnumerable<T>
as the return type. Maybe the original .NET guidelines are just outdated?
Or maybe it's not so clear cut? Are there some tradeoffs to consider? When would it be a good idea to return a more specific type? Is it ever recommended to return a concrete generic type, or only to return a more specific interface like IList<T>
and ReadOnlyCollection<T>
?
There are reasons for both styles:
List<T>
. Maybe you can promise an IList<T>
or a custom collection class (which you can change later).The .NET framework BCL goes with either arrays or custom collection classes. They need to provide a 100% stable API so they need to be careful.
In normal software projects you can change the caller (which the .NET framework guys can't), so you can be more lenient. If you promise too much, and need to change that a year later, you can do that (with some effort).
There is only one thing that is always wrong: Saying that one should always do (1) or (2). Always is always wrong.
Here is an example for a case where specificity is clearly the right choice:
public static T[] Slice<T>(this T[] list, int start, int count)
{
var result = new T[count];
Array.Copy(list, start, result, 0, count);
return result;
}
There is only one reasonable implementation possible, so we can clearly promise that the return type is an array. We will never need to return a list.
On the other hand, a method to return all users from some persistent store might change a lot internally. The data might even be bigger than available memory, which requires streaming. We should probably choose IEnumerable<User>
.
The problem with guidelines around decisions like this is that we developers are susceptible to following them blindly, without thinking. I would argue these issues ought to be considered on a case-by-case basis, weighing the advantages and drawbacks of different possible approaches in each case.
With return types, there are really (at least) two ways of looking at it. One philosophy that I see here on Stack Overflow quite often prefers choosing more generic types in order to give you—the author of the code—more flexibility down the road. For example, if you write a method that returns an IList<T>
and the return value is actually a List<T>
, you reserve the right to later implement your own optimized data structure that implements IList<T>
and return that in a future version.
A different but in my opinion equally valid alternate philosophy would be that returning more generic types simply makes your library less useful, and so you should return more specific types. For instance aggressively returning IEnumerable<T>
all the time will lead to the scenario where end users of your library are constantly calling ToArray()
, ToList()
, etc. I believe this is the basic point your coworker was making when he wrote that "things like Lists and Arrays have extra methods that are useful."
I have sometimes heard the latter approach advocated in words along the lines of "Be specific in what you offer, but generic in what you accept." In other words, take generic parameters but offer specific return values. Whether you agree with this principle or not, it is easy to at least see the reason behind it.
In any event, as I said each case will differ in the details. For instance in my first example of IList<T>
versus List<T>
, you might already be planning to implement a custom data structure and simply haven't written it yet. In this case returning a List<T>
would probably be a poor choice since you already know the interface will be different in a few weeks or months.
On the other hand, you might have some method that always returns a T[]
which you choose to expose only as an IEnumerable<T>
even though the likelihood of this implementation ever changing is very low. In this case opting for the more generic type might be more pleasing to you aesthetically, but it is not likely to actually benefit anyone and in fact deprives the result of a very useful piece of functionality: random access.
So it's always going to be a trade-off, and the best guideline I think you can follow is to always consider your choices and use your judgment to pick the most sensible one for the case in question.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With