在动态双重分派之前检查类型可见性
Check type visibility prior to dynamic double dispatch
使用dynamic
实现双重分派:
public interface IDomainEvent {}
public class DomainEventDispatcher
{
private readonly List<Delegate> subscribers = new List<Delegate>();
public void Subscribe<TEvent>(Action<TEvent> subscriber) where TEvent : IDomainEvent
{
subscribers.Add(subscriber);
}
public void Publish<TEvent>(TEvent domainEvent) where TEvent : IDomainEvent
{
foreach (Action<TEvent> subscriber in subscribers.OfType<Action<TEvent>>())
{
subscriber(domainEvent);
}
}
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (IDomainEvent domainEvent in domainEvents)
{
// Force double dispatch - bind to runtime type.
Publish(domainEvent as dynamic);
}
}
}
public class ProcessCompleted : IDomainEvent { public string Name { get; set; } }
在大多数情况下有效:
var dispatcher = new DomainEventDispatcher();
dispatcher.Subscribe((ProcessCompleted e) => Console.WriteLine("Completed " + e.Name));
dispatcher.PublishQueue(new [] { new ProcessCompleted { Name = "one" },
new ProcessCompleted { Name = "two" } });
Completed one
Completed two
但是如果子classes 对调度代码不可见,这会导致运行时错误:
public static class Bomb
{
public static void Subscribe(DomainEventDispatcher dispatcher)
{
dispatcher.Subscribe((Exploded e) => Console.WriteLine("Bomb exploded"));
}
public static IDomainEvent GetEvent()
{
return new Exploded();
}
private class Exploded : IDomainEvent {}
}
// ...
Bomb.Subscribe(dispatcher); // no error here
// elsewhere, much later...
dispatcher.PublishQueue(new [] { Bomb.GetEvent() }); // exception
RuntimeBinderException
The type 'object' cannot be used as type parameter 'TEvent' in the generic type or method 'DomainEventDispatcher.Publish(TEvent)'
这是一个人为的例子;更现实的是另一个程序集内部的事件。
如何防止此运行时异常?如果那不可行,我如何在 Subscribe
方法中检测到这种情况并快速失败?
编辑: 消除动态转换的解决方案是可以接受的,只要它们不需要知道所有子classes.
How can I prevent this runtime exception?
你真的做不到,这就是 dynamic
的本性。
If that isn't feasible, how can I detect this case in the Subscribe
method and fail fast?
您可以在添加订阅者之前检查 typeof(TEvent).IsPublic
。
也就是说,我不确定您是否真的需要 dynamic
进行双重派遣。如果 subscribers
是 Dictionary<Type, List<Action<IDomainEvent>>>
并且您根据 domainEvent.GetType()
在 Publish(IDomainEvent domainEvent)
中查找订阅者怎么办?
您只需将发布方法更改为:
foreach(var subscriber in subscribers)
if(subscriber.GetMethodInfo().GetParameters().Single().ParameterType == domainEvent.GetType())
subscriber.DynamicInvoke(domainEvent);
更新
您还必须将调用更改为
Publish(domainEvent); //Remove the as dynamic
这样您就不必更改 Publish 的签名
不过我更喜欢我的另一个答案:
更新 2
关于你的问题
I am curious as to why this dynamic invocation works where my original
one fails.
请记住,dynamic 不是一种特殊类型。
基本上是编译器:
1) 用 object
替换它
2)将您的代码重构为更复杂的代码
3)删除编译时检查(这些检查在运行时完成)
如果您尝试替换
Publish(domainEvent as dynamic);
与
Publish(domainEvent as object);
你会得到同样的消息,但这次是在编译时。
错误消息不言自明:
The type 'object' cannot be used as type parameter 'TEvent' in the
generic type or method 'DomainEventDispatcher.Publish(TEvent)'
作为最后的说明。
动态是为特定场景设计的,99,9% 的时候你不需要它,你可以用静态类型代码代替它。
如果你认为你需要它(像上面的例子)你可能做错了什么
由于您的 Subscribe
方法已经具有通用类型,您可以进行以下简单更改:
private readonly List<Action<object>> subscribers = new List<Action<object>>();
public void Subscribe<TEvent>(Action<TEvent> subscriber) where TEvent : class
{
subscribers.Add((object evnt) =>
{
var correctType = evnt as TEvent;
if (correctType != null)
{
subscriber(correctType);
}
});
}
public void Publish(object evnt)
{
foreach (var subscriber in subscribers)
{
subscriber(evnt);
}
}
如果您在发布端和订阅端都缺少编译时类型信息,您仍然可以消除动态转换。看到这个 Expression building example.
与其试图找出动态调用失败的原因,我更愿意专注于提供一个可行的解决方案,因为根据我对合同的理解,你有一个有效的订阅者,因此你应该能够将调用分派给它。
幸运的是,有几个基于非动态调用的解决方案。
通过反射调用Publish
方法:
private static readonly MethodInfo PublishMethod = typeof(DomainEventDispatcher).GetMethod("Publish"); // .GetMethods().Single(m => m.Name == "Publish" && m.IsGenericMethodDefinition);
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var publish = PublishMethod.MakeGenericMethod(domainEvent.GetType());
publish.Invoke(this, new[] { domainEvent });
}
}
通过反射调用subscriber
:
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var eventType = typeof(Action<>).MakeGenericType(domainEvent.GetType());
foreach (var subscriber in subscribers)
{
if (eventType.IsAssignableFrom(subscriber.GetType()))
subscriber.DynamicInvoke(domainEvent);
}
}
}
通过预编译缓存委托调用Publish
方法:
private static Action<DomainEventDispatcher, IDomainEvent> CreatePublishFunc(Type eventType)
{
var dispatcher = Expression.Parameter(typeof(DomainEventDispatcher), "dispatcher");
var domainEvent = Expression.Parameter(typeof(IDomainEvent), "domainEvent");
var call = Expression.Lambda<Action<DomainEventDispatcher, IDomainEvent>>(
Expression.Call(dispatcher, "Publish", new [] { eventType },
Expression.Convert(domainEvent, eventType)),
dispatcher, domainEvent);
return call.Compile();
}
private static readonly Dictionary<Type, Action<DomainEventDispatcher, IDomainEvent>> publishFuncCache = new Dictionary<Type, Action<DomainEventDispatcher, IDomainEvent>>();
private static Action<DomainEventDispatcher, IDomainEvent> GetPublishFunc(Type eventType)
{
lock (publishFuncCache)
{
Action<DomainEventDispatcher, IDomainEvent> func;
if (!publishFuncCache.TryGetValue(eventType, out func))
publishFuncCache.Add(eventType, func = CreatePublishFunc(eventType));
return func;
}
}
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var publish = GetPublishFunc(domainEvent.GetType());
publish(this, domainEvent);
}
}
使用编译 System.Linq.Expressions
延迟创建并按需缓存委托。
目前这个方法应该是最快的了。它也是最接近动态调用实现的,区别在于它的工作原理:)
使用dynamic
实现双重分派:
public interface IDomainEvent {}
public class DomainEventDispatcher
{
private readonly List<Delegate> subscribers = new List<Delegate>();
public void Subscribe<TEvent>(Action<TEvent> subscriber) where TEvent : IDomainEvent
{
subscribers.Add(subscriber);
}
public void Publish<TEvent>(TEvent domainEvent) where TEvent : IDomainEvent
{
foreach (Action<TEvent> subscriber in subscribers.OfType<Action<TEvent>>())
{
subscriber(domainEvent);
}
}
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (IDomainEvent domainEvent in domainEvents)
{
// Force double dispatch - bind to runtime type.
Publish(domainEvent as dynamic);
}
}
}
public class ProcessCompleted : IDomainEvent { public string Name { get; set; } }
在大多数情况下有效:
var dispatcher = new DomainEventDispatcher();
dispatcher.Subscribe((ProcessCompleted e) => Console.WriteLine("Completed " + e.Name));
dispatcher.PublishQueue(new [] { new ProcessCompleted { Name = "one" },
new ProcessCompleted { Name = "two" } });
Completed one
Completed two
但是如果子classes 对调度代码不可见,这会导致运行时错误:
public static class Bomb
{
public static void Subscribe(DomainEventDispatcher dispatcher)
{
dispatcher.Subscribe((Exploded e) => Console.WriteLine("Bomb exploded"));
}
public static IDomainEvent GetEvent()
{
return new Exploded();
}
private class Exploded : IDomainEvent {}
}
// ...
Bomb.Subscribe(dispatcher); // no error here
// elsewhere, much later...
dispatcher.PublishQueue(new [] { Bomb.GetEvent() }); // exception
RuntimeBinderException
The type 'object' cannot be used as type parameter 'TEvent' in the generic type or method 'DomainEventDispatcher.Publish(TEvent)'
这是一个人为的例子;更现实的是另一个程序集内部的事件。
如何防止此运行时异常?如果那不可行,我如何在 Subscribe
方法中检测到这种情况并快速失败?
编辑: 消除动态转换的解决方案是可以接受的,只要它们不需要知道所有子classes.
How can I prevent this runtime exception?
你真的做不到,这就是 dynamic
的本性。
If that isn't feasible, how can I detect this case in the
Subscribe
method and fail fast?
您可以在添加订阅者之前检查 typeof(TEvent).IsPublic
。
也就是说,我不确定您是否真的需要 dynamic
进行双重派遣。如果 subscribers
是 Dictionary<Type, List<Action<IDomainEvent>>>
并且您根据 domainEvent.GetType()
在 Publish(IDomainEvent domainEvent)
中查找订阅者怎么办?
您只需将发布方法更改为:
foreach(var subscriber in subscribers)
if(subscriber.GetMethodInfo().GetParameters().Single().ParameterType == domainEvent.GetType())
subscriber.DynamicInvoke(domainEvent);
更新
您还必须将调用更改为
Publish(domainEvent); //Remove the as dynamic
这样您就不必更改 Publish 的签名
不过我更喜欢我的另一个答案:
更新 2
关于你的问题
I am curious as to why this dynamic invocation works where my original one fails.
请记住,dynamic 不是一种特殊类型。
基本上是编译器:
1) 用 object
替换它
2)将您的代码重构为更复杂的代码
3)删除编译时检查(这些检查在运行时完成)
如果您尝试替换
Publish(domainEvent as dynamic);
与
Publish(domainEvent as object);
你会得到同样的消息,但这次是在编译时。 错误消息不言自明:
The type 'object' cannot be used as type parameter 'TEvent' in the generic type or method 'DomainEventDispatcher.Publish(TEvent)'
作为最后的说明。
动态是为特定场景设计的,99,9% 的时候你不需要它,你可以用静态类型代码代替它。
如果你认为你需要它(像上面的例子)你可能做错了什么
由于您的 Subscribe
方法已经具有通用类型,您可以进行以下简单更改:
private readonly List<Action<object>> subscribers = new List<Action<object>>();
public void Subscribe<TEvent>(Action<TEvent> subscriber) where TEvent : class
{
subscribers.Add((object evnt) =>
{
var correctType = evnt as TEvent;
if (correctType != null)
{
subscriber(correctType);
}
});
}
public void Publish(object evnt)
{
foreach (var subscriber in subscribers)
{
subscriber(evnt);
}
}
如果您在发布端和订阅端都缺少编译时类型信息,您仍然可以消除动态转换。看到这个 Expression building example.
与其试图找出动态调用失败的原因,我更愿意专注于提供一个可行的解决方案,因为根据我对合同的理解,你有一个有效的订阅者,因此你应该能够将调用分派给它。
幸运的是,有几个基于非动态调用的解决方案。
通过反射调用Publish
方法:
private static readonly MethodInfo PublishMethod = typeof(DomainEventDispatcher).GetMethod("Publish"); // .GetMethods().Single(m => m.Name == "Publish" && m.IsGenericMethodDefinition);
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var publish = PublishMethod.MakeGenericMethod(domainEvent.GetType());
publish.Invoke(this, new[] { domainEvent });
}
}
通过反射调用subscriber
:
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var eventType = typeof(Action<>).MakeGenericType(domainEvent.GetType());
foreach (var subscriber in subscribers)
{
if (eventType.IsAssignableFrom(subscriber.GetType()))
subscriber.DynamicInvoke(domainEvent);
}
}
}
通过预编译缓存委托调用Publish
方法:
private static Action<DomainEventDispatcher, IDomainEvent> CreatePublishFunc(Type eventType)
{
var dispatcher = Expression.Parameter(typeof(DomainEventDispatcher), "dispatcher");
var domainEvent = Expression.Parameter(typeof(IDomainEvent), "domainEvent");
var call = Expression.Lambda<Action<DomainEventDispatcher, IDomainEvent>>(
Expression.Call(dispatcher, "Publish", new [] { eventType },
Expression.Convert(domainEvent, eventType)),
dispatcher, domainEvent);
return call.Compile();
}
private static readonly Dictionary<Type, Action<DomainEventDispatcher, IDomainEvent>> publishFuncCache = new Dictionary<Type, Action<DomainEventDispatcher, IDomainEvent>>();
private static Action<DomainEventDispatcher, IDomainEvent> GetPublishFunc(Type eventType)
{
lock (publishFuncCache)
{
Action<DomainEventDispatcher, IDomainEvent> func;
if (!publishFuncCache.TryGetValue(eventType, out func))
publishFuncCache.Add(eventType, func = CreatePublishFunc(eventType));
return func;
}
}
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var publish = GetPublishFunc(domainEvent.GetType());
publish(this, domainEvent);
}
}
使用编译 System.Linq.Expressions
延迟创建并按需缓存委托。
目前这个方法应该是最快的了。它也是最接近动态调用实现的,区别在于它的工作原理:)