简体   繁体   中英

Why is the generic parameter not casting?

I'm missing something basic, but I can't figure it out. Given:

abstract class EventBase {}
class SpecialEvent : EventBase {}

Over in another class I want to allow callers to be able to RegisterFor<SpecialEvent>(x => {...})

public class FooHandler {
{
    internal Dictionary<Type, Action<EventBase>> _validTypes = new Dictionary<Type, Action<EventBase>>();

    internal void RegisterFor<T>(Action<T> handlerFcn) where T: EventBase
    {
        _validTypes.Add(typeof(T), handlerFcn);
    }
 }

However, the _validTypes.Add line does not compile. It cannot convert a Action<T> to an Action<EventBase> . The constraint specifies that T must be derived from EventBase , so what am I misunderstanding?

C# is correct to disallow that. To understand why, consider this situation:

// This is your delegate implementation
void SpecialAction(SpecialEvent e) {
    Console.WriteLine("I'm so special!");
}

// This is another EventBase class
class NotSoSpecialEvent : EventBase {}

void PureEvil(Action<EventBase> handlerFcn) where T: EventBase {
    handlerFcn(new NotSoSpecialEvent()); // Why not?
}

Let's imagine that C# allows you to pass Action<SpecialEvent> for Action<EventBase> . Here is what would happen next:

PureEvil(SpecialAction); // Not allowed

Now PureEvil will try passing NotSoSpecialEvent to delegate SpecialAction that takes SpecialEvent , which must never happen.

Action delegates are contravariant - the type is defined as Action<in T> . That has an implication on how substitution principle works when applied to actions.

Consider two types: B (base) and D (derived). Then Action<B> is more derived than Action<D> . This implies the following behavior:

class B { }
class D : B { }
...
Action<D> derivedAction = ...
Action<B> baseAction = derivedAction;  // Fails
Action<D> derivedAction1 = baseAction; // Succeeds

In your example, with SpecialEvent being the type deriving from EventBase , you are only allowed to assign Action<SpecialEvent> = Action<EventBase> , but not the other way around (as you are attempting).

Since you are already checking the event type before storing the delegates in the dictionary, then you don't have to insist on storing strongly typed delegates - let alone that you cannot insist on strong typing due to contravariance of Action delegates.

You can store anything you like in the dictionary, eg Delegate or plain object and then downcast to a concrete Action<T> when the time comes to fetch the Action delegate from the collection.

public class FooHandler
{
    internal Dictionary<Type, object> _validTypes = 
        new Dictionary<Type, object>();

    internal void RegisterFor<T>(Action<T> handlerFcn) 
        where T: EventBase
    {
        _validTypes.Add(typeof(T), handlerFcn);
    }

    internal Action<T> Find<T>()
        where T : EventBase =>
        (Action<T>)_validTypes[typeof(T)];
 }

Action<SpecialEvent> cannot be used as Action<EventBase> .

Use Delegate to abstract the parameters and then cast it back:

public class FooHandler
{
    internal Dictionary<Type, Delegate> _validTypes = new Dictionary<Type, Delegate>();

    internal void RegisterFor<T>(Action<T> handlerFcn) where T : EventBase
    {
        _validTypes.Add(typeof(T), handlerFcn);
    }

    internal void ExecuteHandler<T>(T value) where T : EventBase
    {
        var handler = (Action<T>)_validTypes[typeof(T)];
        handler(value);
    }
}

Use it like this:

var handler = new Action<SpecialEvent>(ev => { Console.WriteLine("works!"); });
FooHandler.RegisterFor<SpecialEvent>(handler);
FooHandler.ExecuteHandler(new SpecialEvent());

You've got an Action<SpecialEvent> , which is only known to handle SpecialEvent . Action<EventBase> means any EventBase can be passed to it. That makes the conversion unsafe.

In your case, I'd opt for a Dictionary<Type, Delegate> instead, where each T key is paired with an Action<T> value. If you can ensure you only add values of the correct type, you can safely cast the delegate back to Action<T> when you want to invoke it.

What if some Action<EventBase> in the Dictionary is an Action<UnexpectedEvent> instead of an Action<SpecialEvent> ? Action<T> is contravariant, but it is not covariant.

see: https://blogs.msdn.microsoft.com/ericlippert/2007/10/16/covariance-and-contravariance-in-c-part-one/

If you happen to know that the types should work out, and/or want an exception if the types conflict, you can wrap it in another action that performs a cast, like so:

_validTypes.Add(typeof(T), eventBase => handlerFcn((T)eventBase));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM