C# Generics Won't Allow Delegate Type Constraints

C# Generics won't allow Delegate Type Constraints

A number of classes are unavailable as generic contraints - Enum being another.

For delegates, the closest you can get is ": class", perhaps using reflection to check (for example, in the static constructor) that the T is a delegate:

static GenericCollection()
{
if (!typeof(T).IsSubclassOf(typeof(Delegate)))
{
throw new InvalidOperationException(typeof(T).Name + " is not a delegate type");
}
}

C# Delegate Type As Generic Constraint

C# doesn't allow constraining generic type parameters with delegate types. Your only option to validate delegate types is at runtime.

For the same reason, you won't be able to use the += operator inside the CreateCallback method. But if += is moved to the caller (SetupCallbacks), and the CreateCallback only creates and returns the delegate, it can still look quite elegant:

// this code is in SetupCallbacks method
// Action<...> delegates are just examples

TurnManager.Instance.OnTurnStarted +=
CreateCallback<Action<string, int>>(actionType, CardData.TurnStartedMethod);

TurnManager.Instance.OnTurnStopped +=
CreateCallback<Action<string, int, TimeSpan>>(actionType, CardData.TurnStoppedMethod);

Where the CreateCallback method is as follows:

private TDelegate CreateCallback<TDelegate>(Type actionType, string method)
where TDelegate : class
{
if (!typeof(Delegate).IsAssignableFrom(typeof(TDelegate)))
{
throw new InvalidOperationException("Card::SetupCallback - " + typeof(TDelegate).Name + " is not a delegate");
}

MethodInfo methodInfo = actionType.GetMethod(method);

if (methodInfo != null)
{
// the following line will also validate compatibility of delegate types
Delegate nonTypedDelegate = methodInfo.CreateDelegate(typeof(TDelegate));
TDelegate typedDelegate = (TDelegate)(object)nonTypedDelegate;
return typedDelegate;
}

return null;
}

Provided that in my example, TurnManager class looks like this:

public class TurnManager
{
public static TurnManager Instance
{
get { /* ....... */ }
}

public Action<string, int> OnTurnStarted { get; set; }
public Action<string, int, TimeSpan> OnTurnStopped { get; set; }

//... other members ...
}

Why does this Delegate constraint not work with defining an event?

It does not work because you cannot enforce your generic argument to be a non-abstract derived type of MulticastDelegate, which would satisfy the C# event declaration (in other words, you can substitute the Delegate type itself in place of your constrained argument).

Recommended solution:

If you use conventional events, then you don't need to use the delegate constraint at all. Just use the generic EventHandler<T> delegate and substitute only its event argument:

public abstract class TemplateScriptEvent<TEventArgs> : ScriptEvent
where TEventArgs : EventArgs
{
public event EventHandler<TEventArgs> Event;

protected virtual void OnEvent(TEventArgs e) => Event?.Invoke(this, e);
}

Abstract "event" for any delegate types:

If you really want to use any delegate type, then the closest solution you can do is something like this:

public abstract class TemplateScriptDelegate<TDelegate, TResult> : ScriptEvent
where TDelegate : Delegate
{
// this is a simple property rather than an event, but allows +=/-= for concrete delegate types
// (and also simple assignments).
public TDelegate Event { get; set; }

// you must implement this in a more concrete derived type
protected abstract TResult OnEvent();
}

// To be able to use this you need a derived type for each delegate types (which of course,
// can have type arguments for return value and parameters).
// For example, you can have a derived type for Func<T> delegates:
public class TemplateScriptFunc<TResult> : TemplateScriptDelegate<Func<TResult>, TResult>
{
protected override TResult OnEvent() => Event.Invoke();
}

Making a generic type constraint on FuncT

Unfortunately, it looks like you are out of luck. Func<> and Action<> are both delegate types, which cannot be used as a generic type constraint.

This answer sums it up pretty well
C# Generics won't allow Delegate Type Constraints

Generic method with delegate type

You could use Unconstrained Melody - or pinch the bit which allows this.

On the other hand, that's not actually going to expose an Invoke method anyway. What do you want to do which can't be done with:

void MyMethod(Delegate aMethod, params object[] aParams)
{
aMethod.DynamicInvoke(aParams);
}

? I mean, you wouldn't actually have compile-time safety even with the generic version, as the parameters could be of the wrong types etc.

Can we restrict the type be a delegate

A Delegate is a class, and you would normally be able to specify a non-sealed class as a constraint. However, the language specification specifically excludes System.Delegate as a valid constraint in section 10.1.5.

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.

Passing a delegate/event as a ref generic parameter

This problem is tricky to solve because generics won't allow a delegate as a type constraint, so we can't write a generic method that will accept any particular type of delegate.

But we can specify a type constraint on a type parameter within a delegate. So the solution is to get rid of CustomDelegate1 and CustomDelegate2 and instead use a generic version such as CustomDelegate<T> and pass the event arg type as the generic parameter.

Here's how the delegate and event argument definitions might look:

public class CustomEventArgs1 : EventArgs { }

public class CustomEventArgs2 : EventArgs { }

public delegate void CustomDelegate<T>(object sender, T e) where T : EventArgs;

And here is your method:

public void RaiseEventForPresenter<T>(ref CustomDelegate<T> action, T args) where T : EventArgs
{
if (action == null) CreatePresenter();
action(this, args);
}

Cannot cast delegate to a generic type T

The C# language imposes a static check whether a cast from type X to type Y is valid — i.e. whether it makes sense in that the compiler can (to certain degree) guarantee compatibility and reject errors that as clear on compile-time. An unconstrained generic type T and System.Delegate have nothing directly in common. However, when casted to object the compiler knows that every type is esentially an object, so it allows the cast. That doesn't mean a run-time type check won't fail in a particular case.

The as operator is a little bit more permissive, as it won't cause an exception for an otherwise-invalid cast. The compiler is also less-strict in applying static checks. In your particular case this is helpful as you can omit the intermediate cast to object and use as T. However, one this required is that as works on class types only, so you have to apply the where T : class constraint.

So the method then would look like this (simplified):

public T CreateDelegate<T>(…) where T : class
{
return Delegate.CreateDelegate(typeof(T), …) as T;
}

As @usr suggested, a recommended reading is Eric Lippert's blog, e.g. this article on casts and type parameters.

How to store Action delegates with constrained generic type in a type safe collection in C#?

_delegates has to be of type IList<Action<T>>.

Therefore you have to add the <T> where T : ISomething to the class.

Alternatively, do away with the generics and support Action<ISomething> directly.

So your two options are:

public class Delegates<T> where T : ISomething
{
private List<Action<T>> _delegates;

public void AddDelegate(Action<T> action)
{
_delegates.Add(action);
}
}

Or

public class Delegates
{
private List<Action<ISomething>> _delegates;

public void AddDelegate(Action<ISomething> action)
{
_delegates.Add(action);
}
}

Edit
As Sehnsucht points out, there's a third option: wrap the Action<T> delegate inside an Action<ISomething> delegate and then the OP can achieve what he originally wanted.

The reason for this is that whilst T is a "subtype" (implementer) of ISomething, Action<T> is not a subtype of Action<ISomething>, in fact as dcastro explains in his answer, the opposite is actually true (despite that seeming counter-intuitive). Even with casting, you cannot add an instance of Action<T> to a list of Action<ISomething>.



Related Topics



Leave a reply



Submit