C#/.NET 最高效的动态调用方法的方式

3
我们正在开发一个系统,该系统从tcp/ip流中读取命令,然后执行这些命令。命令由对对象的方法调用组成,该对象也由命令中的id标识。您可以将命令视为元素id的信息(定位我们要在其上调用命令的元素)和命令id(定位应在该元素上调用的方法)。此外,我们还需要检查每个命令的某种权限以及如何执行此命令(是否应在新的Thread中启动等)。
这样一个命令调用的示例可能如下所示:
class Callee
{
    public void RegularCall(int command, parameters)
    {
        switch (command)
        {
            case 1: // Comand #1
                // Check if the permissions allow this command to be called.
                // Check if it should be outsourced to the ThreadPool and
                // call it accordingly. +Other Checks.
                // Finally execute command #1.
                break;
            case 2: // Comand #2
                // Check if the permissions allow that command to be called.
                // Check if it should be outsourced to the ThreadPool and
                // call it accordingly. +Other Checks.
                // Finally execute command #2.
                break;
            // Many more cases with various combinations of permissions and
            // Other flags.
        }
    }
}

And somewhere:

static Dictionary<int, Callee> callees = new Dictionary<int, Callee>();

static void CallMethod(int elementId, int commandId, parameters)
{
    callees[elementId].RegularCall(commandId, parameters);
}

然而,这种方法有些不太优雅:

  • 由于反复复制相同的代码,这可能会导致出错。
  • 在某些情况下,很难看到存在哪些命令以及它们的标志。
  • 命令方法充满了检查,这些检查本可以在方法外进行。

我的第一种方法是使用反射,看起来会像这样:

class Callee
{
    [Command(1)]
    [Permissions(0b00111000)]
    [UseThreadPool]
    public void SpeakingNameForCommand1(parameters)
    {
        // Code for command #1.
    }

    [Command(2)]
    [Permissions(0b00101011)]
    public void SpeakingNameForCommand2(parameters)
    {
        // Code for command #2.
    }

    // Again, many more commands.
}

这段代码必须用一些涉及反射的代码进行初始化:

  1. 查找可能代表元素的所有类。
  2. 查找所有具有命令属性的方法等。
  3. 将所有这些信息存储在一个字典中,包括相应的MethodInfo

接收到命令的调用看起来像这样,其中CommandInfo是一个包含调用所需的所有信息的类(MethodInfo、在ThreadPool中运行、权限等):

static Dictionary<int, CommandInfo> commands = new Dictionary<int, CommandInfo>();

static void CallMethod(int elementId, int commandId)
{
    CommandInfo ci = commands[commandId];

    if (ci.Permissions != EVERYTHING_OK)
        throw ...;

    if (ci.UseThreadPool)
        ThreadPool.Queue...(delegate { ci.MethodInfo.Invoke(callees[elementId], params); });
    else
        ci.MethodInfo.Invoke(callees[elementId], params);
}

当我进行微基准测试时,调用MethodInfo.Invoke的速度大约比直接调用慢100倍。问题是:有没有更快的调用这些“命令”方法的方法,而不会失去定义应如何调用这些命令的属性的优雅性?
我还尝试从MethodInfo派生委托。然而,这并不好用,因为我需要能够在Callee类的任何实例上调用该方法,并且不希望为每个可能的元素*命令保留委托的内存。(将会有许多元素。)
只是为了让这一点清楚:MethodInfo.Invoke的速度比包括switch/case语句的函数调用要慢100倍。这不包括遍历所有类、方法和属性所需的时间,因为这些信息已经准备好了。
请不要告诉我其他瓶颈,比如网络情况。它们不是问题。并且它们不是在代码中使用慢速调用的理由。谢谢。

3
如果你正在从网络流中读取数据,瓶颈很可能是网络而不是 MethodInfo.Invoke。优化后者可能在性能上没有明显的差异。 - Michael Liu
1
除了Michael的回答之外,所有的答案都可能归结为“不动态调用方法将是最快的”。 - Patrick Hollweck
1
请考虑Michael Liu提到的内容,同时也可以考虑使用所谓的开放委托来处理实例方法的委托(https://blog.slaks.net/2011/06/open-delegates-vs-closed-delegates.html),如果可能存在“正常”/闭合委托(包含对目标对象实例的引用)数量过多的情况... - user2819245
2
我真的忍不住,你为什么要争论伪代码呢?你怎么知道某个东西是对还是错呢?你甚至没有所有必要的信息来做出那种说法。 - Matthias
2
嘿,看起来Scharle帮我省了写答案的功夫... :-) - user2819245
显示剩余8条评论
2个回答

4

您可以使用开放式委托,它们比MethodInfo.Invoke快约十倍。您可以通过以下方式从MethodInfo创建这样的delegate

delegate void OpenCommandCall(Callee element, parameters);

OpenCommandCall occDelegate = (OpenCommandCall)Delegate.CreateDelegate(typeof(OpenCommandCall), methodInfo));

您可以像这样调用此委托:
occDelegate.Invoke(callee, params);

在这里,callee 是您要调用方法的元素,methodInfo 是该方法的 MethodInfo,而 parameters 则是其他各种参数的占位符。


3
也许你想尝试一下ObjectMethodExecutor
根据Hanselman的说法:
如果您需要通过反射调用类型上的方法,并且该方法可能是异步的,我们有一个助手在 ASP.NET Core 代码库中广泛使用,高度优化和灵活,称为 ObjectMethodExecutor。团队在 MVC 中使用此代码来调用控制器方法,在 SignalR 中使用此代码来调用您的 hub 方法。它处理异步和同步方法。它还处理自定义可等待对象和 F# 异步工作流。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接