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



使用完整的、可工作的代码示例进行编辑

在我的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;
    }
}

为了接收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);
        }
    }
}

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

第二个例子是应用程序使用工厂方法创建一个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);
    }
}

异常指出,我不能将INotification<IMessage>(Publish方法理解发布的消息是什么)强制转换为INotification<ServerMessage>

我曾尝试将INotification接口泛型标记为反变量,如INotification<in TMessageType>,但无法做到这一点,因为我使用TMessageType作为Register方法回调的参数。我应该把接口分成两个单独的接口吗?一个可以注册,一个可以消费?这是最好的选择吗?

任何额外的帮助都将是伟大的。

这里的基本问题是您试图以变体的方式使用类型,但您尝试使用的语法不支持这种方式。感谢您更新的、现在已经完成的(几乎是最小的)代码示例,很明显,您根本无法按照现在的编写方式完成这项工作。

有问题的接口,特别是您要使用的方法(即ProcessMessage()实际上可以声明为协变接口(如果您将Register()方法拆分为单独的接口)。但这样做并不能解决你的问题。

您可以看到,问题是您正试图将INotification<ServerMessage>的实现分配给类型为INotification<IMessage>的变量。请注意,一旦将该实现分配给该类型的变量,调用者就可以将IMessage任何实例传递给该方法,即使不是ServerMessage实例的实例也是如此。但是实际实现期望(不,需要!)ServerMessage的实例。

换言之,您试图编写的代码根本不静态安全。在编译时,它无法保证类型匹配,这不是C#愿意做的事情


一种选择是削弱接口的类型安全性,使其成为非通用的。也就是说,让它始终接受一个IMessage实例。然后,每个实现都必须根据其需要进行强制转换。使用InvalidCastException,编码错误只会在运行时被捕获,但正确的代码运行良好。


另一种选择是设置这种情况,以便知道完整的类型参数。例如,也使PushMessage()成为泛型方法,这样它就可以使用ServerMessage的类型参数而不是IMessage:来调用Publish()

private void OnMessageProcessed(IrcMessage message, IrcCommand command, ICommandFormatter response)
{
    this.OnMessageProcessed(message);
    ServerMessage formattedMessage = (ServerMessage)response.FormatMessage(message, command);
    this.PushMessage(formattedMessage);
}
private void PushMessage<T>(T notification) where T : IMessage
{
    this.notificationManager.Publish(notification);
}

这样,类型参数T将在出现问题的foreach循环中完全匹配。


就我个人而言,我更喜欢第二种方法。我意识到,在您当前的实现中,这是行不通的。但是,IMHO值得重新审视更广泛的设计,看看你是否可以在保留泛型类型的同时实现相同的功能,这样它就可以用来确保编译时类型的安全性。

这里有很长的篇幅,从摆弄您提供的代码开始。。。

使用断点,我可以知道该方法认为T是什么以及侦听器[messageType]的类型是什么吗?

foreach (Notification<T> handler in listeners[messageType])
{
    handler.ProcessMessage(message);
}

因为如果确实是Notification<IMessage>在一边,Notification<ServerMessage>在另一边,那么是的,这是一个分配兼容性问题。

有一个解决方案,但您还没有显示如何构建Notification的代码。我将根据您当前的代码库进行推断。这应该是你所需要的。

public interface INotification<in T> { /* interfacy stuff */ }
public class Notification<T>: INotification<T> { /* classy stuff */ }

然后以这样一种方式修改代码,本质上这被称为:

foreach (INotification<T> handler in listeners[messageType]) { /* loop stuff */ }

其中侦听器[messageType]必须是INotification。

这应该可以避免像编译器抱怨那样将Notification显式转换为Notification。

INotification的接口声明中出现了魔力,T中的关键字短语(抱歉,术语不好)让编译器知道T是反变量的(默认情况下,如果省略,T是不变的,因为类型必须匹配)。

编辑:根据评论,我已经更新了答案,以反映实际编写的代码,而不是我认为的编写的代码。这主要意味着将INotification声明为反变量(在T中),而不是协变(在T外)。

我通过添加另一个间接级别解决了这个问题。根据@PeterDuniho的建议,我将INotification<TMessage>接口拆分为两个单独的接口。通过添加新的INotificationProcessor接口,我可以将侦听器集合从ISubscription更改为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);
}

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);
    }
}

我的NotificationManager现在可以保存INotificationProcessor类型的集合,而不是ISubscription,并调用ProcessMessage(IMessage)方法,而不管其中包含的是IMessage还是ServerMessage

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);
        }
    }
}

最初的应用程序示例现在可以正常工作。

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);
    }
}

感谢大家的帮助。

相关内容

  • 没有找到相关文章

最新更新