在动态双重分派之前检查类型可见性

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 进行双重派遣。如果 subscribersDictionary<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 延迟创建并按需缓存委托。

目前这个方法应该是最快的了。它也是最接近动态调用实现的,区别在于它的工作原理:)