c# - foreach 循环无法将类型转换为它实现的接口(interface)

标签 c# exception covariance

使用完整的、有效的代码示例进行编辑。

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

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

为了演示上面的代码是如何工作的,我有一个简单的控制台应用程序,它订阅来自上面 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 接口(interface)通用标记为逆变,例如 INotification<in TMessageType>但不能那样做,因为我正在消费 TMessageType作为 Register 的参数方法的回调。我应该将接口(interface)分成两个单独的接口(interface)吗?一种可以注册,一种可以消费?这是最好的选择吗?

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

最佳答案

这里的基本问题是您正在尝试以变体方式使用您的类型,但您尝试使用的语法不支持这种方式。感谢您更新且现在完整(并且几乎是最小的)代码示例,很明显您根本无法按照现在编写的方式执行此操作。

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

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

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


一种选择是通过使其成为非泛型来削弱接口(interface)的类型安全性。 IE。让它总是接受 IMessage实例。然后每个实现都必须根据其需要进行转换。编码错误只会在运行时被捕获,带有 InvalidCastException ,但正确的代码可以正常运行。


另一种选择是设置情况,以便知道完整的类型参数。例如,制作 PushMessage()通用方法,以便它可以调用 Publish()使用 ServerMessage 的类型参数而不是 IMessage :

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 中完全匹配在遇到问题的地方循环。


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

关于c# - foreach 循环无法将类型转换为它实现的接口(interface),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28575411/

相关文章:

c++ - 为什么在从 win32 计时器回调中抛出时不调用我的析构函数?

c# - 协变/逆变是否适用于不实现公共(public)接口(interface)的隐式可转换类型?

c# - 仅在一台电脑上出现异常,在其他电脑上工作正常

c# - 鼠标位置如何转换为滚动控件?

exception - 我无法将验证消息设置为约束

java - Java 中有很多异常是糟糕的设计吗?

java - 如何使用泛型在 Java 中将 List<?> 转换为 List<T>?

c# - C# 数组的协变和逆变

C#,快速泛型问题

C# 是否有一种紧凑的方法来获取二维数组中的多个坐标?