Thursday, March 08, 2007

Anonymous Method Subtleties

As explained quite clearly by Raymond Chen here, the compiler generates a few different things when confronted with an anonymous method, based on what the method does. If the method doesn't refer to anything other than its parameters, it generates a static method on the containing class. If it refers to class instance members, you get an instance method. When the method refers to other variables in the lexically-enclosing method (basically when you need a closure), the compiler generates a sealed nested class to track those variables and puts the method there.

We have an interesting framework scenario where controls register event handlers with a custom base page. In the "add" section of the event in the page, we need to get a reference to the control that is adding the handler. Generally this is available by reading "value.Target" (value is a Delegate since we're inside the "add" of an event). However, if you happen to be using an anonymous method as your handler and you happen to change it to require a closure, "value.Target" will instead point at the compiler-generated nested class. This nested class inherits from nothing, implements no interfaces, and has a field for each captured variable as well as a reference to the instance the anonymous method is defined in.

As far as I can tell, there isn't a general way to get from the instance of the nested class back to the instance of the surrounding class. Furthermore, there isn't a general, easy way to detect that "value.Target" is giving you a generated class rather than the one you were expecting. Since anonymous methods are a compiler trick, there's no run time information available via reflection to tell you whether a method came from an anonymous method or not.

We still needed to get at the control, so we had to guess. We guess (with very high probability) that if "value.Target" doesn't inherit from Control, it must be an anonymous method. We then do a few sanity checks to make sure (including checking that the class is sealed, private, nested, and has a [CompilerGenerated] attribute on it). Then, to get at the original instance, we find the field of "value.Target" whose type matches the outer type of our class, and then read its value. It looks a little something like this:

if (d.Target is Control)
{
return d.Target;
}

Type targetType = d.Target.GetType();
if (!(targetType.IsNestedPrivate && targetType.IsSealed
&& (targetType.GetCustomAttributes(
typeof(CompilerGeneratedAttribute), false).Length == 1)))

{
//you've hit the Anonymous Method case with something
//that doesn't appear to be an anonymous method
throw new InvalidOperationException("Error.");
}

Type outerClassType = targetType.DeclaringType;
FieldInfo outerField = Array.Find(targetType.GetFields(),
delegate(FieldInfo field)
{
return field.FieldType.Equals(outerClassType);
});

if (outerField == null)
{
//see note on the throw exception above
throw new InvalidOperationException("Error.");
}

return outerField.GetValue(d.Target);


This would be a lot easier if the compiler-generated class implemented an interface or something with a single method returning object that pointed back to the enclosing instance. Then I could just say:

return (d.Target is IAnonymousMethodState ?
((IAnonymousMethodState)d.Target).EnclosingInstance :
d.Target);

I'm going to suggest this at the Connect site and see what happens.
Edit: Suggested here

No comments: