使用.NET 3.5调用多个表达式

3

替代方案

虽然我(对于这个项目仍然)只能使用 .NET 3.5,但我已经成功地使用了 Expression Trees 的 DLR 版本。该版本发布在 Apache 许可证第 2.0 版下。

这增加了对所有(也许更多或更少,但可能不是).NET 4.0+ 表达式的支持,例如我需要用于此问题的 BlockExpression

源代码可以在 GitHub 上找到。


原始问题

在我的当前项目中,我正在编译一个具有可变数量参数的表达式树。我有一系列需要调用的 Expressions。在 .NET 4.0+ 中,我只需使用 Expression.Block 即可实现此目的,但是在此项目中我只能使用 .NET 3.5。

现在,我已经找到了一个巨大的 hack 来解决这个问题,但我不认为这是最好的方法。

代码:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

class Program
{
    struct Complex
    {
        public float Real;
        public float Imaginary;
    }

    // Passed to all processing functions
    class ProcessContext
    {
        public ConsoleColor CurrentColor;
    }

    // Process functions. Write to console as example.
    static void processString(ProcessContext ctx, string s)
    { Console.ForegroundColor = ctx.CurrentColor; Console.WriteLine("String: " + s); }
    static void processAltString(ProcessContext ctx, string s)
    { Console.ForegroundColor = ctx.CurrentColor; Console.WriteLine("AltString: " + s); }
    static void processInt(ProcessContext ctx, int i)
    { Console.ForegroundColor = ctx.CurrentColor; Console.WriteLine("Int32: " + i); }
    static void processComplex(ProcessContext ctx, Complex c)
    { Console.ForegroundColor = ctx.CurrentColor; Console.WriteLine("Complex: " + c.Real + " + " + c.Imaginary + "i"); }

    // Using delegates to access MethodInfo, just to simplify example.
    static readonly MethodInfo _processString = new Action<ProcessContext, string>(processString).Method;
    static readonly MethodInfo _processAltString = new Action<ProcessContext, string>(processAltString).Method;
    static readonly MethodInfo _processInt = new Action<ProcessContext, int>(processInt).Method;
    static readonly MethodInfo _processComplex = new Action<ProcessContext, Complex>(processComplex).Method;

    static void Main(string[] args)
    {
        var methodNet40 = genNet40();
        var methodNet35 = genNet35();

        var ctx = new ProcessContext();
        ctx.CurrentColor = ConsoleColor.Red;

        methodNet40(ctx, "string1", "string2", 101, new Complex { Real = 5f, Imaginary = 10f });
        methodNet35(ctx, "string1", "string2", 101, new Complex { Real = 5f, Imaginary = 10f });


        // Both work and print in red:

        // String: string1
        // AltString: string2
        // Int32: 101
        // Complex: 5 + 10i
    }

    static void commonSetup(out ParameterExpression pCtx, out ParameterExpression[] parameters, out Expression[] processMethods)
    {
        pCtx = Expression.Parameter(typeof(ProcessContext), "pCtx");

        // Hard-coded for simplicity. In the actual code these are reflected.
        parameters = new ParameterExpression[]
        {
            // Two strings, just to indicate that the process method
            // can be different between the same types.
            Expression.Parameter(typeof(string), "pString"),
            Expression.Parameter(typeof(string), "pAltString"),
            Expression.Parameter(typeof(int), "pInt32"),
            Expression.Parameter(typeof(Complex), "pComplex")
        };

        // Again hard-coded. In the actual code these are also reflected.
        processMethods = new Expression[]
        {
            Expression.Call(_processString, pCtx, parameters[0]),
            Expression.Call(_processAltString, pCtx, parameters[1]),
            Expression.Call(_processInt, pCtx, parameters[2]),
            Expression.Call(_processComplex, pCtx, parameters[3]),
        };
    }

    static Action<ProcessContext, string, string, int, Complex> genNet40()
    {
        ParameterExpression pCtx;
        ParameterExpression[] parameters;
        Expression[] processMethods;
        commonSetup(out pCtx, out parameters, out processMethods);

        // What I'd do in .NET 4.0+
        var lambdaParams = new ParameterExpression[parameters.Length + 1]; // Add ctx
        lambdaParams[0] = pCtx;
        Array.Copy(parameters, 0, lambdaParams, 1, parameters.Length);

        var method = Expression.Lambda<Action<ProcessContext, string, string, int, Complex>>(
            Expression.Block(processMethods),
            lambdaParams).Compile();

        return method;
    }

    static Action<ProcessContext, string, string, int, Complex> genNet35()
    {
        ParameterExpression pCtx;
        ParameterExpression[] parameters;
        Expression[] processMethods;
        commonSetup(out pCtx, out parameters, out processMethods);

        // Due to the lack of the Block expression, the only way I found to execute
        // a method and pass the Expressions as its parameters. The problem however is
        // that the processing methods return void, it can therefore not be passed as
        // a parameter to an object.

        // The only functional way I found, by generating a method for each call,
        // then passing that as an argument to a generic Action<T> invoker with
        // parameter T that returns null. A super dirty probably inefficient hack.

        // Get reference to the invoke helper
        MethodInfo invokeHelper =
            typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
            .Single(x => x.Name == "invokeHelper" && x.IsGenericMethodDefinition);

        // Route each processMethod through invokeHelper<T>
        for (int i = 0; i < processMethods.Length; i++)
        {
            // Get some references
            ParameterExpression param = parameters[i];
            Expression process = processMethods[i];

            // Compile the old process to Action<T>
            Type delegateType = typeof(Action<,>).MakeGenericType(pCtx.Type, param.Type);
            Delegate compiledProcess = Expression.Lambda(delegateType, process, pCtx, param).Compile();

            // Create a new expression that routes the Action<T> through invokeHelper<T>
            processMethods[i] = Expression.Call(
                invokeHelper.MakeGenericMethod(param.Type),
                Expression.Constant(compiledProcess, delegateType),
                pCtx, param);
        }

        // Now processMethods execute and then return null, so we can use it as parameter
        // for any function. Get the MethodInfo through a delegate.
        MethodInfo call2Helper = new Func<object, object, object>(Program.call2Helper).Method;

        // Start with the last call
        Expression lambdaBody = Expression.Call(call2Helper,
            processMethods[processMethods.Length - 1],
            Expression.Constant(null, typeof(object)));

        // Then add all the previous calls
        for (int i = processMethods.Length - 2; i >= 0; i--)
        {
            lambdaBody = Expression.Call(call2Helper,
                processMethods[i],
                lambdaBody);
        }

        var lambdaParams = new ParameterExpression[parameters.Length + 1]; // Add ctx
        lambdaParams[0] = pCtx;
        Array.Copy(parameters, 0, lambdaParams, 1, parameters.Length);

        var method = Expression.Lambda<Action<ProcessContext, string, string, int, Complex>>(
            lambdaBody,
            lambdaParams).Compile();

        return method;
    }

    static object invokeHelper<T>(Action<ProcessContext, T> method, ProcessContext ctx, T parameter)
    {
        method(ctx, parameter);
        return null;
    }

    static object call2Helper(object p1, object p2) { return null; }
}

我想找到一个好的替代品的主要原因是不想在我们的代码库中加入这个丑陋的hack(如果没有合适的替代品,我会这样做)。
但是另一方面,这也非常浪费资源,并且在一个可能比较弱的客户端机器上运行,每秒钟可能会运行几千次。现在它不会破坏或提高我们游戏的性能,但这并不是可以忽视的。每种方法的函数调用数量如下:
.NET 4.0:1次编译和N次方法调用。 .NET 3.5:1 + N次编译和3N + 1次方法调用(尽管可以优化到约2N + log N)。
测试性能(在发布版中)显示调用过程的差异为3.6倍。在调试版本中,速度差异约为6倍,但这并不太重要,我们开发人员的机器更强大。
2个回答

5

即使保持相同(或类似)的基本策略,只要稍微重构一下代码,就可以大大简化代码。

编写自己的Block实现,该实现接受一系列表达式并创建一个表示调用它们所有的单个表达式。

为此,您将拥有一个私有实现方法,该方法接受多种操作并调用它们所有,将所有可用的表达式转换为传递给该方法的操作,然后您可以返回表示该方法调用的表达式:

//TODO come up with a better name
public class Foo
{
    private static void InvokeAll(Action[] actions)
    {
        foreach (var action in actions)
            action();
    }
    public static Expression Block(IEnumerable<Expression> expressions)
    {
        var invokeMethod = typeof(Foo).GetMethod("InvokeAll",
            BindingFlags.Static | BindingFlags.NonPublic);
        var actions = expressions.Select(e => Expression.Lambda<Action>(e))
            .ToArray();
        var arrayOfActions = Expression.NewArrayInit(typeof(Action), actions);
        return Expression.Call(invokeMethod, arrayOfActions);
    }
}

这不需要提前编译任何表达式,更重要的是它允许您将创建表达式块的逻辑与使用分离,使您能够根据框架版本的不同轻松地引入/排除它。

这看起来很不错,它是一种类似的策略,但摆脱了我围绕它的丑陋(大部分)。我不知道你可以像这样使用Expression.Lambda,太棒了。 - Aidiakapi
@usr 您的意思是创建一个表达式,其中包含foreach循环遍历行动并调用Invoke吗?您可以这样做,但似乎并不更容易或更有效。 - Servy
他似乎有一组表达式作为输入,并希望调用它们全部。因此,只需将它们全部编译为“Action”,然后从C#中调用“InvokeAll”,而不是从表达式中调用。也许我没有理解问题,我不太清楚什么是演示代码和什么是真实的。 - usr
@usr 啊,我现在明白了。你可能可以这么做。这里的主要优势是它与“Expression.Block”可互换,只需更改那个语句即可在不同框架之间切换,而无需更显著地重构代码堆栈。 - Servy
@Aidiakapi,通过编译函数调用InvokeAll会比直接从C#中调用它慢,因为有间接性。顺便说一下,也许你的架构需要这样做。 - usr
显示剩余4条评论

0

我认为没有必要为每个要调用的操作编译一个lambda表达式。让处理函数返回除void以外的其他内容。这样你就可以生成:

static object Dummy(object o1, object o2, object o3, ...) { return null; }

Dummy(func1(), func2(), func3());

Dummy 应该被内联。

即使需要编译多个 lambda,也不需要在运行时接受委托调用。您可以访问已编译 lambda 的 Delegate.Method 并直接发出对该方法的静态调用。


处理函数来自于一个无法更改的接口,因此我不能仅使它们返回一个值。从API设计的角度来看,这也没有意义,因为返回值只会被丢弃。 - Aidiakapi

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