When lazy evaluation attacks
I just had a lovely object lesson in lazy evaluation of Iterators. I wanted to have method that would return an enumerator over an encapsulated set after doing some sanity checking:
public IEnumerable<Subscription> Filter(Func<Subscription, bool> filter) {
if(filter == null) {
throw new ArgumentNullException("filter","cannot execute with a null filter");
}
foreach(var subInfo in _subscriptions.ToArray()) {
Subscription sub;
try {
var subDoc = XDocFactory.LoadFrom(subInfo.Path, MimeType.TEXT_XML);
sub = new Subscription(subDoc );
if(filter(sub) {
continue;
}
} catch(Exception e) {
_log.Warn(string.Format("unable to retrieve subscription for path '{0}'", subInfo.Path), e);
continue;
}
yield return sub;
}
}
I was testing registering a subscription in the repository with this code:
IEnumerable<Subscription> query;
try {
query = _repository.Filter(handler);
} catch(ArgumentException e) {
return;
}
foreach(var sub in query) {
...
}
And the test would throw a ArgumentNullException
because handler was null. What? But, but i clearly had a try/catch
around it! Well, here's where clever bit me. By using yield
, the method had turned into an enumerator instead of a method call that returned an enumerable. That means that the method body would get squirreled away into an enumerator closure that would not get executed until the first MoveNext()
. And that in turn meant that my sanity check on handler didn't happen at Filter()
but at the first iteration of the foreach
.
Instead of doing "return an Iterator for subscriptions", I needed to do "check the arguments" and then "return an Iterators for subscriptions" as a separate action. This can be accomplished by factoring the yield
into a method called by Filter()
instead of being in Filter()
itself:
public IEnumerable<Subscription> Filter(Func<Subscription, bool> filter) {
if(filter == null) {
throw new ArgumentException("cannot execute with a null filter");
}
return BuildSubscriptionEnumerator(Func<Subscription, bool> filter);
}
public IEnumerable<Subscription> BuildSubscriptionEnumerator(Func<Subscription, bool> filter) {
foreach(var subInfo in _subscriptions.ToArray()) {
Subscription sub;
try {
var subDoc = XDocFactory.LoadFrom(subInfo.Path, MimeType.TEXT_XML);
sub = new Subscription(subDoc );
if(filter(sub) {
continue;
}
} catch(Exception e) {
_log.Warn(string.Format("unable to retrieve subscription for path '{0}'", subInfo.Path), e);
continue;
}
yield return sub;
}
}
Now the sanity check happens at Filter()
call time, while the enumeration of subscription still only occurs as its being iterated over, allowing for additional filtering and Skip/Take
additions without having to traverse the entire possible set.