2

使用以下方式实现双重调度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

4 回答 4

2

如何防止此运行时异常?

实在不行,这就是天性dynamic

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

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

也就是说,我不确定您是否真的需要dynamic双重调度。如果subscribers是 a并且您基于Dictionary<Type, List<Action<IDomainEvent>>>查找订户怎么办?Publish(IDomainEvent domainEvent)domainEvent.GetType()

于 2016-02-25T23:23:49.057 回答
1

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

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”不能用作泛型类型或方法“DomainEventDispatcher.Publish(TEvent)”中的类型参数“TEvent”

作为最后的说明。
dynamic 是为特定场景设计的,99,9% 的时间你不需要它,你可以用静态类型的代码替换它。
如果你认为你需要它(如上述情况),你可能做错了什么

于 2016-02-28T19:27:34.097 回答
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

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

于 2016-03-02T19:27:07.830 回答
-1

由于您的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);
    }
}

如果您在发布端和订阅端都缺少编译时类型信息,您仍然可以消除动态转换。请参阅此表达式构建示例。

于 2016-02-29T22:20:06.823 回答