foreach循环不能将类型转换为它实现的接口

Joh*_*ger 8 c# exception covariance

使用完整,有效的代码示例进行编辑.

在我的IRC应用程序中,应用程序从IRC服务器接收内容.内容被发送到工厂并且工厂吐出IMessage可以由应用程序的表示层使用的对象.的IMessage接口和单一的实现如下所示.

public interface IMessage
{
    object GetContent();
}

public interface IMessage<out TContent> : IMessage where TContent : class
{
    TContent Content { get; }
}

public class ServerMessage : IMessage<string>
{
    public ServerMessage(string content)
    {
        this.Content = content;
    }

    public string Content { get; private set; }

    public object GetContent()
    {
        return this.Content;
    }
}
Run Code Online (Sandbox Code Playgroud)

要接收IMessage对象,表示层将订阅在我的域层中发布的通知.通知系统将订阅者集合迭代到指定的IMessage实现,并向订阅者发出回调方法.

public interface ISubscription
{
    void Unsubscribe();
}

public interface INotification<TMessageType> : ISubscription where TMessageType : class, IMessage
{
    void Register(Action<TMessageType, ISubscription> callback);
    void ProcessMessage(TMessageType message);
}

internal class Notification<TMessage> : INotification<TMessage> where TMessage : class, IMessage
{
    private Action<TMessage, ISubscription> callback;

    public void Register(Action<TMessage, ISubscription> callbackMethod)
    {
        this.callback = callbackMethod;
    }

    public void Unsubscribe()
    {
        this.callback = null;
    }

    public void ProcessMessage(TMessage message)
    {
        this.callback(message, this);
    }
}

public class NotificationManager
{
    private ConcurrentDictionary<Type, List<ISubscription>> listeners =
        new ConcurrentDictionary<Type, List<ISubscription>>();

    public ISubscription Subscribe<TMessageType>(Action<TMessageType, ISubscription> callback) where TMessageType : class, IMessage
    {
        Type messageType = typeof(TMessageType);

        // Create our key if it doesn't exist along with an empty collection as the value.
        if (!listeners.ContainsKey(messageType))
        {
            listeners.TryAdd(messageType, new List<ISubscription>());
        }

        // Add our notification to our listener collection so we can publish to it later, then return it.
        var handler = new Notification<TMessageType>();
        handler.Register(callback);

        List<ISubscription> subscribers = listeners[messageType];
        lock (subscribers)
        {
            subscribers.Add(handler);
        }

        return handler;
    }

    public void Publish<T>(T message) where T : class, IMessage
    {
        Type messageType = message.GetType();
        if (!listeners.ContainsKey(messageType))
        {
            return;
        }

        // Exception is thrown here due to variance issues.
        foreach (INotification<T> handler in listeners[messageType])
        {
            handler.ProcessMessage(message);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

为了演示上述代码的工作原理,我有一个简单的Console应用程序,它可以订阅上述ServerMessage类型的通知.控制台应用程序首先通过将ServerMessage对象Publish<T>直接传递给方法来发布.这没有任何问题.

第二个示例让app使用工厂方法创建IMessage实例.然后将IMessage实例传递给Publish<T>方法,导致我的方差问题抛出InvalidCastException.

class Program
{
    static void Main(string[] args)
    {
        var notificationManager = new NotificationManager();
        ISubscription subscription = notificationManager.Subscribe<ServerMessage>(
            (message, sub) => Console.WriteLine(message.Content));

        notificationManager.Publish(new ServerMessage("This works"));
        IMessage newMessage = MessageFactoryMethod("This throws exception");
        notificationManager.Publish(newMessage);

        Console.ReadKey();
    }

    private static IMessage MessageFactoryMethod(string content)
    {
        return new ServerMessage(content);
    }
}
Run Code Online (Sandbox Code Playgroud)

该异常声明我无法将INotification<IMessage>(发布方法理解发布的消息是什么)转换为INotification<ServerMessage>.

我试图将INotification接口标记为逆变,INotification<in TMessageType>但不能这样做,因为我正在使用TMessageTypeRegister方法的回调作为参数.我应该将接口分成两个独立的接口吗?一个可以注册,一个可以消费?这是最好的选择吗?

任何额外的帮助都会很棒.

Joh*_*ger 1

我通过添加另一个间接级别解决了这个问题。遵循@PeterDuniho 的建议后,我将INotification<TMessage>界面分为两个单独的界面。通过添加新INotificationProcessor接口,我可以将侦听器集合从 an 更改ISubscription为 an INotificationProcessor,然后将侦听器集合作为 INotificationProcessor 进行迭代。

public interface ISubscription
{
    void Unsubscribe();
}

public interface INotification<TMessageType> : ISubscription where TMessageType : class, IMessage
{
    void Register(Action<TMessageType, ISubscription> callback);
}

public interface INotificationProcessor
{
    void ProcessMessage(IMessage message);
}
Run Code Online (Sandbox Code Playgroud)

INotificationProcessor实现同时实现INotificationProcessorINotification<TMessageType>。这允许Notification下面的类将提供的 IMessage 转换为适当的通用类型以进行发布。

internal class Notification<TMessage> : INotificationProcessor, INotification<TMessage> where TMessage : class, IMessage
{
    private Action<TMessage, ISubscription> callback;

    public void Register(Action<TMessage, ISubscription> callbackMethod)
    {
        this.callback = callbackMethod;
    }

    public void Unsubscribe()
    {
        this.callback = null;
    }

    public void ProcessMessage(IMessage message)
    {
        // I can now cast my IMessage to T internally. This lets
        // subscribers use this and not worry about handling the cast themselves. 
        this.callback(message as TMessage, this);
    }
}
Run Code Online (Sandbox Code Playgroud)

My现在可以保存类型NotificationManager集合,而不是调用该方法,无论传入的内容是 an还是 a 。INotificationProcessorISubscriptionProcessMessage(IMessage)IMessageServerMessage

public class NotificationManager
{
    private ConcurrentDictionary<Type, List<INotificationProcessor>> listeners =
        new ConcurrentDictionary<Type, List<INotificationProcessor>>();

    public ISubscription Subscribe<TMessageType>(Action<TMessageType, ISubscription> callback) where TMessageType : class, IMessage
    {
        Type messageType = typeof(TMessageType);

        // Create our key if it doesn't exist along with an empty collection as the value.
        if (!listeners.ContainsKey(messageType))
        {
            listeners.TryAdd(messageType, new List<INotificationProcessor>());
        }

        // Add our notification to our listener collection so we can publish to it later, then return it.
        var handler = new Notification<TMessageType>();
        handler.Register(callback);

        List<INotificationProcessor> subscribers = listeners[messageType];
        lock (subscribers)
        {
            subscribers.Add(handler);
        }

        return handler;
    }

    public void Publish<T>(T message) where T : class, IMessage
    {
        Type messageType = message.GetType();
        if (!listeners.ContainsKey(messageType))
        {
            return;
        }

        // Exception is thrown here due to variance issues.
        foreach (INotificationProcessor handler in listeners[messageType])
        {
            handler.ProcessMessage(message);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

原始应用程序示例现在可以正常运行。

class Program
{
    static void Main(string[] args)
    {
        var notificationManager = new NotificationManager();
        ISubscription subscription = notificationManager.Subscribe<ServerMessage>(
            (message, sub) => Console.WriteLine(message.Content));

        notificationManager.Publish(new ServerMessage("This works"));
        IMessage newMessage = MessageFactoryMethod("This works without issue.");
        notificationManager.Publish(newMessage);

        Console.ReadKey();
    }

    private static IMessage MessageFactoryMethod(string content)
    {
        return new ServerMessage(content);
    }
}
Run Code Online (Sandbox Code Playgroud)

感谢大家的帮助。