c# - 在运行时调用通用接口(interface)方法的有效方法

标签 c# performance generics reflection event-handling

我正在研究某种 EventSourcing 架构,我的应用程序中有 2 个主要概念 - eventshandlers .

事件示例:

class NewRecordCreated: EventMessage {...}

有些处理程序看起来像:

class WriteDBHandler: IEventHandler<NewRecordCreated>, IEventHandler<RecordUpdated> { 
    public void Handle(NewRecordCreated eventMessage) {...}
    public void Handle(RecordUpdated eventMessage) {...} 
}

而且我还有队列协议(protocol)的自定义实现,该协议(protocol)将事件分派(dispatch)给适当的处理程序。所以基本上在应用程序启动时,我会解析程序集并根据类型在事件和处理程序之间创建映射。

因此,当我实际将事件分派(dispatch)给处理程序时,我根据事件类型获取处理程序类型链 - 类似于 var handlerChain = [typeof(WriteDbHandler), typeof(LogHandler), typeof(ReadModelUpdateHandler)]对于这些处理程序中的每一个,我都需要调用它的实例,然后将其转换为适当的接口(interface)( IEventHandler<> )然后调用 Handle方法。

但我无法转换为通用接口(interface),因为这是不可能的。我考虑过实现接口(interface)的非通用版本的选项,但每次都添加额外的方法实现对我来说似乎很不愉快,尤其是在没有任何真正原因的情况下。

我考虑过动态调用或反射,但这两种变体似乎都存在性能问题。也许你可以建议我一些合适的选择?

最佳答案

使用反射

而不是尝试转换为 IEventHandler<> ,您可以改用反射来获取对需要调用的方法的引用。下面的代码就是一个很好的例子。为了简洁起见,它简化了“队列协议(protocol)”,但它应该足以说明您需要做的反射。

class MainClass
{
    public static void Main(string [] args)
    {
        var a = Assembly.GetExecutingAssembly();
        Dictionary<Type, List<Type>> handlerTypesByMessageType = new Dictionary<Type, List<Type>>();

        // find all types in the assembly that implement IEventHandler<T>
        // for some value(s) of T
        foreach (var t in a.GetTypes())
        {
            foreach (var iface in t.GetInterfaces())
            {
                if (iface.GetGenericTypeDefinition() == typeof(IEventHandler<>))
                {
                    var messageType = iface.GetGenericArguments()[0];
                    if (!handlerTypesByMessageType.ContainsKey(messageType))
                        handlerTypesByMessageType[messageType] = new List<Type>();
                    handlerTypesByMessageType[messageType].Add(t);
                }
            }
        }

        // get list of events
        var messages = new List<EventMessage> {
            new NewRecordCreated("one"),
            new RecordUpdated("two"),
            new RecordUpdated("three"),
            new NewRecordCreated("four"),
            new RecordUpdated("five"),
        };

        // process all events
        foreach (var msg in messages)
        {
            var messageType = msg.GetType();
            if (!handlerTypesByMessageType.ContainsKey(messageType))
            {
                throw new NotImplementedException("No handlers for that type");
            }

            if (handlerTypesByMessageType[messageType].Count < 1)
            {
                throw new NotImplementedException("No handlers for that type");
            }

            // look up the handlers for the message type
            foreach (var handlerType in handlerTypesByMessageType[messageType])
            {
                var handler = Activator.CreateInstance(handlerType);
                // look up desired method by name and parameter type
                var handlerMethod = handlerType.GetMethod("Handle", new Type[] { messageType });
                handlerMethod.Invoke(handler, new object[]{msg});
            }
        }
    }
}

我编译了它并在我的机器上运行它并得到了我认为正确的结果。

使用运行时代码生成

如果反射对于您的目的来说不够快,您可以为每个输入消息类型即时编译代码并执行它。 System.Reflection.Emit namespace 具有执行此操作的功能。 您可以定义一个动态方法(不要与 dynamic 关键字混淆,后者是另外一回事),如果 IL 操作码将按顺序运行列表中的每个处理程序,则发出一个序列。

public static Dictionary<Type, Action<EventMessage>> GenerateHandlerDelegatesFromTypeLists(Dictionary<Type, List<Type>> handlerTypesByMessageType)
{
    var handlersByMessageType = new Dictionary<Type, Action<EventMessage>>();
    foreach (var messageType in handlerTypesByMessageType.Keys)
    {
        var handlerTypeList = handlerTypesByMessageType[messageType];

        if (handlerTypeList.Count < 1)
            throw new NotImplementedException("No handlers for that type");

        var method =
            new DynamicMethod(
                "handler_" + messageType.Name,
                null,
                new [] { typeof(EventMessage) });
        var gen = method.GetILGenerator();
        foreach (var handlerType in handlerTypeList)
        {
            var handlerCtor = handlerType.GetConstructor(new Type[0]);
            var handlerMethod =
                handlerType.GetMethod("Handle", new Type[] { messageType });

            // create an object of the handler type
            gen.Emit(OpCodes.Newobj, handlerCtor);
            // load the EventMessage passed as an argument
            gen.Emit(OpCodes.Ldarg_0);
            // call the handler object's Handle method
            gen.Emit(OpCodes.Callvirt, handlerMethod);
        }

        gen.Emit(OpCodes.Ret);

        var del = (Action<EventMessage>)method.CreateDelegate(
            typeof(Action<EventMessage>));

        handlersByMessageType[messageType] = del;
    }
}

然后,不用使用 handlerMethod.Invoke(handler, new object[]{msg}) 调用处理程序,您只需像其他任何人一样调用委托(delegate),使用 handlersByMessageType[messageType](msg) .

完整代码 list here .

实际的代码生成是在 GenerateHandlerDelegatesFromTypeLists 中完成的方法。 它实例化一个新的 DynamicMethod , 获取其关联的 ILGenerator ,然后依次为每个处理程序发出操作码。 对于每个处理程序类型,它将实例化该处理程序类型的新对象,将事件消息加载到堆栈上,然后执行Handle。处理程序对象上该消息类型的方法。 这当然是假设处理程序类型都具有零参数构造函数。 但是,如果您需要将参数传递给构造函数,则必须对其进行大量修改。

还有其他方法可以进一步加快速度。 如果放宽为每条消息创建一个新处理程序对象的要求,那么您可以在生成代码时创建对象并加载它们。 在这种情况下,替换 gen.Emit(OpCodes.Newobj, handlerCtor)gen.Emit(OpCodes.Ldobj, handlerObjectsByType[handlerType]) . 这给了你两个好处: 1. 你在避免对每条消息进行分配 2. 填充 handlerObjectsByType 时,您可以以任何方式实例化对象字典。您甚至可以使用带有参数或工厂方法的构造函数。

关于c# - 在运行时调用通用接口(interface)方法的有效方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36992031/

相关文章:

typescript - 有没有办法以数字形式获取元组的键?

c# - Windows 窗体进度条 : Easiest way to start/stop marquee?

c# - 加密 URL 中的路由数据

c++ - pthread互斥体的开销?

c# - 如何重载 Collection(Of T) 的 Items 访问器?

java - 类型不匹配 : cannot convert MyClass<E> to MyClass<E>

c# - 获取进程内存的图像

c# - 不使用for循环将数据写入xml

python - 如何检测矩形中的矩形?

objective-c - Objective C 类别以及执行和打字的速度