在动态双重调度之前检查类型可见性

时间:2016-02-23 21:19:49

标签: c# dynamic access-modifiers 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" } });
  

完成一个

     

完成了两次

但是如果子类对调度代码不可见,则会导致运行时错误:

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

     

类型'object'不能用作泛型类型或方法'DomainEventDispatcher.Publish(TEvent)'中的类型参数'TEvent'

这是一个人为的例子;一个更现实的事件将是另一个集会内部的事件。

如何防止此运行时异常?如果这不可行,我如何在Subscribe方法中检测到这种情况并快速失败?

编辑:消除动态广播的解决方案是可以接受的,只要它们不需要知道所有子类的访问者风格的类。

4 个答案:

答案 0 :(得分:2)

  

如何防止此运行时异常?

你真的不能,这是dynamic的本质。

  

如果这不可行,我如何在Subscribe方法中检测到这种情况并快速失败?

您可以在添加订阅者之前检查typeof(TEvent).IsPublic

那就是说,我不确定你是否真的需要dynamic进行双重调度。如果subscribersDictionary<Type, List<Action<IDomainEvent>>>并且您根据Publish(IDomainEvent domainEvent)domainEvent.GetType()中查找了订阅者,该怎么办?

答案 1 :(得分:1)

我没有试图找出动态调用失败的原因,而是集中精力提供一个可行的解决方案,因为我理解合同的方式,你有一个有效的订阅者,因此你应该能够调用它。

幸运的是,有一些非动态的基于呼叫的解决方案。

通过反思调用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按需创建和缓存代理。

到目前为止,这种方法应该是最快的。它也是最接近动态调用实现的,它的不同之处在于:)

答案 2 :(得分:0)

您所要做的就是将发布方法更改为:

foreach(var subscriber in subscribers) 
    if(subscriber.GetMethodInfo().GetParameters().Single().ParameterType == domainEvent.GetType())
         subscriber.DynamicInvoke(domainEvent);

<强>更新
您还必须将呼叫更改为

 Publish(domainEvent); //Remove the as dynamic

这样您就不必更改Publish的签名

我更喜欢我的另一个答案: C# subscribe to events based on parameter type?

更新2
关于你的问题

  

我很好奇为什么这个动态调用适用于我的原始版本   一个失败。

请记住,动态不是特殊类型 基本上编译器:
1)用对象替换它 2)重构代码到更复杂的代码
3)删除编译时检查(这些检查在运行时完成)

如果您尝试替换

Publish(domainEvent as dynamic);

Publish(domainEvent as object);

您将收到相同的消息,但这次是在编译时。 错误消息是不言自明的:

  

类型'object'不能用作类型参数'TEvent'   泛型类型或方法'DomainEventDispatcher.Publish(TEvent)'

作为最后一点。
动态是针对特定场景设计的,99.9%的时间不需要它,您可以使用静态类型代码替换它。
如果您认为自己需要它(如上述情况),那么您可能做错了

答案 3 :(得分:-1)

由于您的SUMIFS方法已经具有泛型类型,因此您可以轻松进行更改:

Subscribe

如果您缺少发布和订阅方的编译时类型信息,您仍然可以消除动态转换。见Expression building example.