I was fiddling around the other day with generics, generic collections, and anonymous methods, looking to see if they could be combined to provide a compact, elegant utility class and/or method that would make facilitate firing & forgetting an event asynchronously using the thread pool.
The closest I could come was a solution that would let me do this to fire the event:
Utils.FireEvent(
SomeEvent,
delegate(SomeEventHandler handler)
{
handler.BeginInvoke(argshere, delegate(IAsyncResult ar) { handler.EndInvoke(ar); }, null);
}
);
But I don't find that to be particularly readable. And it's certainly not more compact or elegant than the straight forward approach:
SomeEventHandler handlers = SomeEvent;
if( handlers != null ) {
foreach( SomeEventHandler handler in handlers.GetInvocationList() ) {
handler.BeginInvoke(argshere, delegate(IAsyncResult ar) { handler.EndInvoke(ar); }, null);
}
}
So in the end, I chucked the idea and stuck with the straight forward approach to using the thread pool to fire and forget an event. But along the way, I ran into a restriction (constraint) on the way type parameter constraints work that I had not bumped into before.
Here's how my Utils.FireEvent method started out:
public class Utils {
public static void FireEvent<TDelegate>( TDelegate eventHandler, Action<TDelegate> itemHandler ) where TDelegate : Delegate
{
if( eventHandler == null ) {
return; // Nothing to do.
}
TDelegate[] handlers =
Array.ConvertAll( // 1
eventHandler.GetInvocationList(), // 2
delegate( Delegate d ) { return (TDelegate)d; } // 3
);
Array.ForEach(handlers, itemHandler); // 4
}
}
My intention was to use Array.ConvertAll (line 1) to convert the Delegate[] array returned by Delegate.GetInvocationList() (line 2) to a more specifically typed array of TDelegate references (handlers) that could then be passed to Array.ForEach (line 4). The conversion from Delegate to TDelegate would be performed by the anonymous method passed to ConvertAll on line 3. My thinking had been that since I constrainted TDelegate to a derivative of System.Delegate (where TDelegate : Delegate), the compiler would ensure that callers didn't try to pass some non-delegate argument to this method. The typecast on line 3 (from Delegate to TDelegate) could certainly still fail, but the caller would just be getting what they deserved :-)
But the above approach doesn't compile. The compiler issues the following error for the constraint declaration:
Constraint cannot be special class 'System.Delegate'.
Sure enough, the C# 2.0 specification has this to say (20.7, Constraints):
A class-type constraint must satisfy the following rules:
- The type must be a class type.
- The type must not be sealed.
- The type must not be one of the following types: System.Array, System.Delegate, System.Enum, or System.ValueType.
- The type must not be object. Because all types derive from object, such a constraint would have no effect if it were permitted.
- At most one constraint for a given type parameter can be a class type.
I can't find any further discussion of why a type parameter cannot be constrained to deriving directly or indirectly from System.Delegate. (For the record, System.MulticastDelegate isn't allowed either.) So the reasoning behind that constraint on constraints is still unknown to me. If anyone has some background on this, I'd appreciate your comments.
My next thought was to back off a notch with the constraint and just use a reference type constraint:
public class Utils {
public static void FireEvent<TDelegate>( TDelegate eventHandler, Action<TDelegate> itemHandler ) where TDelegate : class
{
...
}
}
This change eliminated the error about my illegal constraint, but still results in an error on line 3:
delegate( Delegate d ) { return (TDelegate)d; } // 3
Cannot convert type 'System.Delegate' to 'TDelegate'.
Unlike the first error I encountered, which informed me there was something special about Delegate that prevented the use I tried, this error gives me a little less to go on. My expectation had been that since I'd constrained TDelegate to be a reference type of some sort, that the typecast operation I was going to attempt on line 3 might or might not succeed - just like any other typecast. If d was in fact type compatible with TDelegate, the cast would work. Otherwise, the caller would get InvalidCastException. That seems reasonable with me.
As before, I went looking through the C# 2.0 spec to see if there was an explanation. The closest I could come was this sentence in section 20.7.4, Conversions involving type parameters:
The above rules do not permit a direct explicit conversion from an unconstrained type parameter to a non-interface type, which might be surprising.
The spec goes on to describe a scenario where trying to use an unconstrained type parameter in this fashion doesn't make sense. And I buy that. But I don't have an unconstrained type parameter. I've constrained TDelegate to being a reference type of one sort or another. So I'm still not clear on why the typecast syntax of line 3 should be disallowed.
Odder still is the fact that, although the typecast conversion above results in a compiler error, the following variation works just fine:
delegate( Delegate d ) { return(d as TDelegate); } // 3
Other than how a failure to perform the requested conversion from System.Delegate to TDelegate is conveyed to me and my caller (InvalidCastException versus a null reference), both variations are attempting the same type conversion. So it seems to me that either both forms should be allowed, or both forms disallowed. Allowing one, but not the other, doesn't make sense to me.
So if someone could shed some light on either or both of these two restrictions on type parameters and their constraints, I'd appreciate your comments.
Posted
Dec 06 2005, 09:09 AM
by
mike-woodring