Constraints on constraints

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

Comments

Cata wrote re: Constraints on constraints
on 02-10-2006 1:27 AM
this does not works
Control.Invoke(delegate{ //Code }));
this works
Control.Invoke(new MethodInvoker(delegate{ //Code }));

so encapsulate you anonymous method in a delegate type
like new Action<Delegate>(delegate( Delegate d ) { return (TDelegate)d; })

lots of delegates ;)
Sebastian Redl wrote re: Constraints on constraints
on 02-11-2006 2:40 PM
I'm in no way a C# expert, and have never worked with C# generics, but I've done quite a bit of work with Java generics.
In Java, the cast would be permitted, but - since the type parameter is simply non-existent at runtime - would simply be ignored.
Fact is, if you constrain a C# parameter to reference types, this is different from any other constraint. With normal constraints, you constrain a type to be from a particular subtree of the type hierarchy. With a reference constraint, you instead exclude a particular subtree (the one deriving from System.ValueType). Thus, the underlying common type of the generic is still Object, and the type might as well be unconstrained.
It seems that C#, instead of ignoring such a no-op as Java does, requires the compiler to emit an error, to avoid luring the programmer into a false sense of security.
this.DataBindings.Add(new Binding( wrote Strongly Typed DynamicMethods
on 03-29-2006 2:12 PM
Chance wrote re: Constraints on constraints
on 12-05-2006 12:46 AM
All delegate types are considered special classes that cannot be specified as type parameters. Doing so would prevent compile-time validation on the call to Event() because the signature of the event firing is unknown with the data types System.Delegate and System.MulticastDelegate. The same restriction occurs for any enum type.
#$%@^! wrote re: Constraints on constraints
on 03-14-2007 12:01 PM
Uck, I hate when languages get in my way like this: "I'm the programmer, you're the language, just shut up and do as you're told!" Nonetheless, C# is what I'm required to write in, and write in it I will... but a strong type system is supposed to aid the programmer, not prevent the implementation of ideas. Now I need to waste time figuring out how to work around another of C#'s "helpful" restrictions. I'd rather be using a weakly typed language than this...
rhencke wrote re: Constraints on constraints
on 08-10-2007 10:35 AM
Here is the workaround I ended up with. It's not typesafe, but it works pretty well:

public static void FireEvent<T>(object sender, object handler, T args) where T : EventArgs
{
if (handler != null) ((MulticastDelegate)handler).DynamicInvoke(sender, args);
}
Julien Couvreur wrote re: Constraints on constraints
on 08-23-2007 9:33 PM
I ran into the same two issues (limitation on constraints for type parameters and limitation on cast) today.

You can work around the second one by using an intermediary local variable:
delegate( Delegate d ) { object temp = d; return (TDelegate)temp; }

Add a Comment

(required)  
(optional)
(required)  
Remember Me?