foreach 循环无法将类型转换为它实现的接口
foreach loop can not cast type to the interface it implements
使用完整、有效的代码示例进行编辑。
在我的 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>
方法来发布。这没有任何问题。
第二个示例让应用程序使用工厂方法创建一个 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
方法的参数回调。我应该将接口分成两个单独的接口吗?一种可以注册,一种可以消费?这是最好的选择吗?
任何额外的帮助都会很棒。
从摆弄您提供的代码开始,这里很长一段时间...
使用断点,我可以知道方法认为 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 */ }
其中 listeners[messageType] 必须是 INotification。
这应该可以防止像编译器抱怨的那样将您的 Notification 显式转换为 Notification 的需要。
魔术发生在 INotification 的接口声明中,in T 关键短语(糟糕的术语,抱歉),让编译器知道 T 是逆变的(默认情况下,如果你省略了,T 是不变的,因为类型必须匹配)。
编辑:根据评论,我更新了答案以反映实际编写的代码,而不是我 认为的书面。这主要意味着将 INotification 声明为逆变(在 T 中)而不是协变(在 T 中)。
这里的基本问题是您试图以不同的方式使用您的类型,但您尝试使用的语法不支持这种方式。感谢您更新且现在完整(并且几乎是最小的)代码示例,很明显您根本无法按照现在编写的方式执行此操作。
有问题的接口,特别是您要使用的方法(即 ProcessMessage()
、实际上可以 声明为协变接口(如果您拆分Register()
方法到一个单独的接口)。但是这样做并不能解决你的问题。
你看,问题是你试图将 INotification<ServerMessage>
的实现分配给类型为 INotification<IMessage>
的变量。请注意,一旦将实现分配给该类型的变量,调用者就可以将 IMessage
的 any 实例传递给方法,即使不是 ServerMessage
。但是实际的实现期望(不,要求!)一个 ServerMessage
.
的实例
换句话说,您尝试编写的代码根本不是静态安全的。它无法在编译时保证类型匹配,而这不是 C# 愿意做的事情。
一种选择是通过使其成为非泛型来削弱接口的类型安全性。 IE。只是让它总是接受一个 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
循环中完全匹配。
就个人而言,我更喜欢第二种方法。我意识到在您当前的实施中,这是行不通的。但恕我直言,可能值得重新审视更广泛的设计,看看您是否可以在始终保留泛型类型的同时完成相同的功能,以便它可用于确保编译时类型安全。
我通过添加另一个间接级别解决了这个问题。听从@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
实现同时实现了 INotificationProcessor
和 INotification<TMessageType>
。这允许下面的 Notification
class 将提供的 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);
}
}
感谢大家的帮助。
使用完整、有效的代码示例进行编辑。
在我的 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>
方法来发布。这没有任何问题。
第二个示例让应用程序使用工厂方法创建一个 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
方法的参数回调。我应该将接口分成两个单独的接口吗?一种可以注册,一种可以消费?这是最好的选择吗?
任何额外的帮助都会很棒。
从摆弄您提供的代码开始,这里很长一段时间...
使用断点,我可以知道方法认为 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 */ }
其中 listeners[messageType] 必须是 INotification。
这应该可以防止像编译器抱怨的那样将您的 Notification 显式转换为 Notification 的需要。
魔术发生在 INotification 的接口声明中,in T 关键短语(糟糕的术语,抱歉),让编译器知道 T 是逆变的(默认情况下,如果你省略了,T 是不变的,因为类型必须匹配)。
编辑:根据评论,我更新了答案以反映实际编写的代码,而不是我 认为的书面。这主要意味着将 INotification 声明为逆变(在 T 中)而不是协变(在 T 中)。
这里的基本问题是您试图以不同的方式使用您的类型,但您尝试使用的语法不支持这种方式。感谢您更新且现在完整(并且几乎是最小的)代码示例,很明显您根本无法按照现在编写的方式执行此操作。
有问题的接口,特别是您要使用的方法(即 ProcessMessage()
、实际上可以 声明为协变接口(如果您拆分Register()
方法到一个单独的接口)。但是这样做并不能解决你的问题。
你看,问题是你试图将 INotification<ServerMessage>
的实现分配给类型为 INotification<IMessage>
的变量。请注意,一旦将实现分配给该类型的变量,调用者就可以将 IMessage
的 any 实例传递给方法,即使不是 ServerMessage
。但是实际的实现期望(不,要求!)一个 ServerMessage
.
换句话说,您尝试编写的代码根本不是静态安全的。它无法在编译时保证类型匹配,而这不是 C# 愿意做的事情。
一种选择是通过使其成为非泛型来削弱接口的类型安全性。 IE。只是让它总是接受一个 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
循环中完全匹配。
就个人而言,我更喜欢第二种方法。我意识到在您当前的实施中,这是行不通的。但恕我直言,可能值得重新审视更广泛的设计,看看您是否可以在始终保留泛型类型的同时完成相同的功能,以便它可用于确保编译时类型安全。
我通过添加另一个间接级别解决了这个问题。听从@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
实现同时实现了 INotificationProcessor
和 INotification<TMessageType>
。这允许下面的 Notification
class 将提供的 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);
}
}
感谢大家的帮助。