I used to create interfaces with IEnumerable<T>
as return type, whenever I want to specify that a particular output is read-only. I like it as it's minimalistic, hides implementation details and decouples a callee from a caller.
But recently a colleague of mine argued that IEnumerable<T>
should be kept for scenarios which involve lazy evaluation only, as otherwise it's unclear for a caller method, where the exception handling should take it's place -- around a method call or around an iteration. Then for the eager evaluation cases with a read-only output I should use a ReadOnlyCollection
.
Sounds quite reasonable for me, but what would you recommend? Would you agree with that convention for IEnumerable? Or are there better methods for exception handling around IEnumerable?
Just in case if my question is unclear, I made a sample class illustrating the problem. Two methods in here have exactly the same signature, but they require different exception handling:
public class EvilEnumerable
{
IEnumerable<int> Throw()
{
throw new ArgumentException();
}
IEnumerable<int> LazyThrow()
{
foreach (var item in Throw())
{
yield return item;
}
}
public void Run()
{
try
{
Throw();
}
catch (ArgumentException)
{
Console.WriteLine("immediate throw");
}
try
{
LazyThrow();
}
catch (ArgumentException)
{
Console.WriteLine("No exception is thrown.");
}
try
{
foreach (var item in LazyThrow())
{
//do smth
}
}
catch (ArgumentException)
{
Console.WriteLine("lazy throw");
}
}
}
Update 1. The question is not limited to ArgumentException only. It's about best practices for making friendly class interfaces, that tell you whether they return lazy evaluated result or not, because this influences the exception-handling approach.
The real problem here is the deferred execution. In the case of argument-checking, you can do this by adding a second method:
IEnumerable<int> LazyThrow() {
// TODO: check args, throwing exception
return LazyThrowImpl();
}
IEnumerable<int> LazyThrowImpl() {
// TODO: lazy code using yield
}
Exceptions happen; even for non-deferred result (List<T>
, for example) you can get errors (perhaps if another thread adjusts the list while you are iterating). The above approach lets you do as much as possible ahead of time to reduce the unexpected side-effects of yield
and deferred execution.