Zeritor Zeritor - 1 month ago 8
C# Question

Unable to cast/match types when using generics in interfaces, why?

In IMyMessage.cs

public interface IMyMessage
{
}


In IMyMessageReceiver.cs

public interface IMyMessageReceiver<T> where T: IMyMessage
{
void HandleMessage(T message);
void Subscribe();
}


In MyMessagePublisher.cs

public static class MyMessagePublisher
{
private static Dictionary<Type, List<IMyMessageReceiver<IMyMessage>>> _subscribers;

static MyMessagePublisher
{
_subscribers = new Dictionary<Type, List<IMyMessageReceiver<IMyMessage>>>();
}

public static function Subscribe<T>(IMyMessageReceiver<T> receiver) where T: IMyMessage
{
Type messageType = typeof (T);
List<IMyMessageReceiver<IMyMessage>> listeners;

if(!_subscribers.TryGetValue(messageType, out listeners))
{
// no list found, so create it
List<IMyMessageReceiver<T>> newListeners = new List<IMyMessageReceiver<T>>();
// ERROR HERE: Can't convert List<IMyMessageReceiver<T>> to List<IMyMessageReceiver<IMyMessage>>
_subscribers.add(messageType, newListeners);

}

// I would then find the right list and add the receiver it to it but haven't got this far
}
}


So my hope was to use a bunch of 'IMyMessages' and 'IMyMessageReceivers' to pass messages around. I did a hard coded approach earlier but got sick of 100 different publish/subscrive function names so I figured I'd wrap it all nicely in generics.

My problem is that I can't get the code to work when using generics. Even though I specify the Type T will be of IMyMessage, I cannot use T anywhere where IMyMessage is expected. Maybe I'm just used to base/extended classes as it would work fine there. I've tried various approaches from casting, to being really generic, yet I always run in to the same issue.

Answer Source

OK here's how I can see it working. Since you're trying to use covariance in a way that is not supported, you'll need to avoid using generics in a few spots. But doing so won't lose any type-safety.

Create a non-generic IMessageReceiver interface so that the types that can't use the generic parameter can use this instead:

public interface IMyMessageReceiver
{
    void HandleMessage(IMyMessage message);

    void Subscribe();
}

public interface IMyMessageReceiver<in T> : IMyMessageReceiver
    where T : IMyMessage
{
    void HandleMessage(T message);
}

You can create a base class to simplify things if you'd like:

public abstract class MyMessageReceiverBase<T> : IMyMessageReceiver<T>
    where T : IMyMessage
{
    public abstract void HandleMessage(T message);

    public void HandleMessage(IMyMessage message)
    {
        if (!(message is T))
            throw new InvalidOperationException();
        HandleMessage((T)message);
    }

    public abstract void Subscribe();
}

Then you can change IMyMessageListeners to use the non-generic version, since it doesn't really need the generic type anyways:

public interface IMyMessageListeners
{
    void Add(IMyMessageReceiver receiver);

    // I added this since I think this is how you're going to use it
    void Send(IMyMessage message);
}

The concrete of this class looks like this:

public class MyMessageListeners : IMyMessageListeners
{
    readonly List<IMyMessageReceiver> _list = new List<IMyMessageReceiver>();

    public void Add(IMyMessageReceiver receiver)
    {
        _list.Add(receiver);
    }

    public void Send(IMyMessage message)
    {
        foreach (var listener in _list)
            listener.HandleMessage(message);
    }
}

Then (finally), your static class will look like this:

public static class MyMessagePublisher
{
    static readonly Dictionary<Type, IMyMessageListeners> _subscribers = new Dictionary<Type, IMyMessageListeners>();

    // I added this too, since I think this is how you intend to use it
    public static void Publish<T>(T message) where T : IMyMessage
    {
        Type messageType = typeof(T);
        IMyMessageListeners listeners;

        if (_subscribers.TryGetValue(messageType, out listeners))
            listeners.Send(message);
    }

    public static void Subscribe<T>(IMyMessageReceiver<T> receiver) where T : IMyMessage
    {
        Type messageType = typeof(T);
        IMyMessageListeners listeners;

        if (!_subscribers.TryGetValue(messageType, out listeners))
        {
            // no list found, so create it
            listeners = new MyMessageListeners();
            _subscribers.Add(messageType, listeners);
        }

        listeners.Add(receiver);
    }
}

And you can use your static class like so:

MyMessagePublisher.Subscribe(new FooMessageReceiver());
MyMessagePublisher.Publish(new FooMessage());