【问题标题】:Check type visibility prior to dynamic double dispatch在动态双重调度之前检查类型可见性
【发布时间】:2016-02-23 21:19:49
【问题描述】:

使用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 方法中检测到这种情况并快速失败?

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

【问题讨论】:

标签: c# dynamic access-modifiers double-dispatch


【解决方案1】:

如何防止此运行时异常?

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

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

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

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

【讨论】:

  • 谢谢基思!我确实发现 Action&lt;IDomainEvent&gt; 不起作用 - Action&lt;T&gt; 中的 T 是逆变的,所以 Add 失败。
  • 因此我不确定如何调用从字典/列表返回的Delegate
  • IsVisible 在测试中似乎比IsPublic 工作得更好。这是一个选择;缺点是它不允许internal(对这个程序集)事件。
  • @dahlbyk 你的回答不正确,但你的建议是
  • 诀窍在于@GeorgeVovos 链接到的内容:Add(i =&gt; subscriber((TEvent)i))
【解决方案2】:

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

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% 的时间你不需要它,你可以用静态类型的代码替换它.
如果您认为您需要它(如上述情况),您可能做错了什么

【讨论】:

  • 这似乎适用于我的简单示例。我还必须将方法签名更改为public void Publish(IDomainEvent domainEvent),我很好奇为什么这个动态调用在我原来的调用失败的地方有效。
【解决方案3】:

与其试图找出动态调用失败的原因,我会专注于提供一个可行的解决方案,因为根据我对合同的理解,你有一个有效的订阅者,因此你应该能够将调用发送给它。

幸运的是,有几个基于非动态调用的解决方案。

通过反射调用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 按需延迟创建和缓存委托。

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

【讨论】:

    【解决方案4】:

    由于您的 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.

    【讨论】:

      猜你喜欢
      • 2015-01-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-02
      • 2018-01-03
      • 2015-02-10
      • 2014-02-05
      • 2011-03-20
      相关资源
      最近更新 更多