Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the generic parameter not casting?

Tags:

c#

.net-core

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?

like image 810
AlG Avatar asked Feb 23 '18 23:02

AlG


People also ask

Can you cast generic type Java?

The Java compiler won't let you cast a generic type across its type parameters because the target type, in general, is neither a subtype nor a supertype.

How do you provide a generic parameterized type?

In order to use a generic type we must provide one type argument per type parameter that was declared for the generic type. The type argument list is a comma separated list that is delimited by angle brackets and follows the type name. The result is a so-called parameterized type.

Does generics eliminate the use of casting?

As mentioned previously, generics can eliminate the requirement for casting.

How do you indicate that a class has a generic type parameter?

A generic type is declared by specifying a type parameter in an angle brackets after a type name, e.g. TypeName<T> where T is a type parameter.


2 Answers

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.

like image 118
Sergey Kalinichenko Avatar answered Oct 25 '22 06:10

Sergey Kalinichenko


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, e.g. 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)];
 }
like image 31
Zoran Horvat Avatar answered Oct 25 '22 06:10

Zoran Horvat