morleyc
morleyc

Reputation: 2431

Convert generic parameter with 'where' type constraint not possible?

I have a base class:

public abstract class DomainEventSubscriber<T> where T : DomainEvent
{
    public abstract void HandleEvent(T domainEvent);
    public Type SubscribedToEventType() { return typeof(T); }
}

And a class which stores the DomainEventSubscriber references:

public class DomainEventPublisher
{
    private List<DomainEventSubscriber<DomainEvent>> subscribers;

    public void Subscribe<T>(DomainEventSubscriber<T> subscriber)
        where T : DomainEvent
    {
        DomainEventSubscriber<DomainEvent> eventSubscriber;
        eventSubscriber = (DomainEventSubscriber<DomainEvent>)subscriber;

        if (!this.Publishing)
        {
            this.subscribers.Add(eventSubscriber);
        }
    }
}

Even though the Subscribe method type is constrained, I cannot convert from DomainEventSubscriber<T> subscriber where T : DomainEvent to DomainEventSubscriber<DomainEvent>:

eventSubscriber = (DomainEventSubscriber<DomainEvent>)subscriber;

How would I go about performing this conversion, or am I setting myself up for a nasty code smell?

Upvotes: 7

Views: 1156

Answers (3)

Ken Kin
Ken Kin

Reputation: 4693

There're good answers for the solution, here I'm point out a little bit for the simple reason why you cannot make it work.

The generic base classes can be inherited, however, with different type parameters are just different classes. Consider List<int> and List<String>. Although they both have the generic definition of List`1, but they take different type parameter; none of them are the base class of the other. What your method does, would be like the following:

public void Subscribe<T>(List<T> subscriber) where T: struct {
    var eventSubscriber=(List<int>)subscriber;

    // ... 
}

this would not compile, and even you change declaration for compile-able as:

public void Subscribe<T>(List<T> subscriber) where T: struct {
    var eventSubscriber=(List<int>)(subscriber as object);

    // ... 
}

and pass an instance of List<byte> would be invalid cast at runtime.

Thus even you've specified the constraints, it just doesn't work.

Beside the existing answers, an alternative way is define a base class(can also be abstract) for the generic class, and make your Subscribe take the base class. However, this would require that you move the abstract method to the base class and change the method signature as a generic method; so it might not be applicable for you.

For knowing the type variance, and assignment ability, you might want to have a look of my Q&A style question:

How to find the minimum covariant type for best fit between two types?

You can use it to get some information about the types, for debugging, and I wish that would be helpful to you for understanding about types.

Upvotes: 3

Daniel A.A. Pelsmaeker
Daniel A.A. Pelsmaeker

Reputation: 50306

Covariance

You need an interface with a covariant type parameter T to be able to cast it to a base type of T. For example, IEnumerable<out T> is such an interface. Note the out keyword, which means T is covariant and can therefore only appear in output positions (e.g. as return values and getters). Because of covariance, you can cast an IEnumerable<Dolphin> to IEnumerable<Mammal>: an enumerable sequence of dolphins is surely also an enumerable sequence of mammals.

Contravariance

However, you cannot make DomainEventSubscriber<T> an interface IDomainEventSubscriber<out T> as T then appears in the input position of HandleEvent. You could make it an interface IDomainEventSubscriber<in T>.

Note the in keyword, which means T is contravariant and can only appear in input positions (e.g. as method parameters). For example, IEqualityComparer<in T> is such an interface. Because of contravariance, you can cast an IEqualityComparer<Mammal> to IEqualityComparer<Dolphin>: if it can compare mammals, then surely it can compare dolphins as they are mammals.

But this also does not solve your problem since you can cast a contravariant type parameter only to a more derived type, and you want to cast it to base type.


Solution

I advise you to create a non-generic IDomainEventSubscriber interface and derive your current class from that:

public interface IDomainEventSubscriber
{
    void HandleEvent(DomainEvent domainEvent);
    Type SubscribedToEventType();
}

public abstract class DomainEventSubscriber<T> : IDomainEventSubscriber
    where T : DomainEvent
{
    void IDomainEventSubscriber.HandleEvent(DomainEvent domainEvent)
    {
        if (domainEvent.GetType() != SubscribedToEventType())
            throw new ArgumentException("domainEvent");

        HandleEvent((T)domainEvent);
    }

    public abstract void HandleEvent(T domainEvent);

    public Type SubscribedToEventType() { return typeof(T); }
}

And then use the IDomainEventSubscriber internally instead of DomainEventSubscriber<DomainEvent>:

public class DomainEventPublisher
{
    private List<IDomainEventSubscriber> subscribers;

    public void Subscribe<T>(DomainEventSubscriber<T> subscriber)
        where T : DomainEvent
    {
        if (!this.Publishing)
        {
            this.subscribers.Add(eventSubscriber);
        }
    }
}

Upvotes: 5

Matthew Watson
Matthew Watson

Reputation: 109567

Here's a slightly different take using an interface:

public class DomainEvent
{
}

// The 'in' isn't actually needed to make this work, but it can be added anyway:

public interface IDomainEventSubscriber<in T> where T: DomainEvent
{
    void HandleEvent(T domainEvent);
    Type SubscribedToEventType();
}

public abstract class DomainEventSubscriber<T>: IDomainEventSubscriber<T> where T: DomainEvent
{
    public abstract void HandleEvent(T domainEvent);
    public Type SubscribedToEventType()
    {
        return typeof(T);
    }
}

public class DomainEventPublisher
{
    private List<DomainEventSubscriber<DomainEvent>> subscribers;

    public void Subscribe<T>(IDomainEventSubscriber<T> subscriber)
        where T: DomainEvent
    {
        DomainEventSubscriber<DomainEvent> eventSubscriber;
        eventSubscriber = (DomainEventSubscriber<DomainEvent>)subscriber;

        if (!this.Publishing)
        {
            this.subscribers.Add(eventSubscriber);
        }
    }
}

Upvotes: 1

Related Questions