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

时间:2015-02-18 03:22:29

标签: 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;
    }
}

要接收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>方法来发布。这没有任何问题。

第二个示例让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);
    }
}

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

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

对此的任何其他帮助都会很棒。

3 个答案:

答案 0 :(得分:2)

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

有问题的界面,特别是你想要使用的方法(即ProcessMessage() 实际上可以被声明为协变界面(如果你拆分{{1} }方法进入一个单独的接口)。但这样做不会解决你的问题。

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

的实例

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


一种选择是通过使接口非通用来削弱接口的类型安全性。即只是让它总是接受一个ServerMessage实例。然后每个实现都必须根据其需要进行转换。编码错误只能在运行时捕获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循环。


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

答案 1 :(得分:1)

在这里长时间伸展,摆弄你提供的代码......

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

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

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

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

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中)。

答案 2 :(得分:1)

我通过添加另一个间接层来解决这个问题。在听了@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);
    }
}

感谢大家的帮助。