I was reading a C# book in which the author (some dude named Jon Skeet) implements a Where
function like
public static IEnumerable<T> Where<T> ( this IEnumerable<T> source, Funct<T,bool> predicate )
{
if ( source == null || predicate == null )
{
throw new ArgumentNullException();
}
return WhereImpl(source, predicate);
}
public static IEnumerable<T> WhereImpl<T> ( IEnumerable <T> source, Func<T,bool> predicate )
{
foreach ( T item in source )
{
if ( predicate(item) )
{
yield return item;
}
}
}
Now, I fully understand how this works and that it's equivalent to
public static IEnumerable<T> Where<T> ( this IEnumerable<T> source, Funct<T,bool> predicate )
{
if ( source == null || predicate == null )
{
throw new ArgumentNullException();
}
foreach ( T item in source )
{
if ( predicate(item) )
{
yield return item;
}
}
}
which brings up the question of why would one separate these into 2 functions given that there would be memory/time overhead and of course more code. I always validate parameters and if I start writing like this example then I'll be writing twice as much code. Is there some school of thought which holds that validation and implementation should be separate functions?
The reason is that the iterator block is always lazy. Unless you call GetEnumerator()
and then MoveNext()
, the code in the method won't get executed.
In other words, consider this call to your "equivalent" method:
var ignored = OtherEnumerable.Where<string>(null, null);
No exception is thrown, because you're not calling GetEnumerator()
and then MoveNext()
. Compare that with my version where the exception is thrown immediately regardless of how the return value is used... because it only calls the method with the iterator block after validating eagerly.
Note that async/await has similar issues - if you have:
public async Task FooAsync(string x)
{
if (x == null)
{
throw new ArgumentNullException(nameof(x));
}
// Do some stuff including awaiting
}
If you call this, you'll end up getting a faulted Task
- rather than a NullReferenceException
being thrown. If you await the returned Task
, then the exception will be thrown, but that may not be where you called the method. That's okay in most cases, but worth knowing about.
See more on this question at Stackoverflow