C# - 使用任意委托类型的表达式树创建 lambda 函数

C# - Creating lambda functions using expression trees of an arbitrary delegate type

我正在尝试创建一个任意类型的运行时 lambda 函数,它将提供给它的参数收集到一个对象列表中,并将它们传递给另一个 void Method(List<object> list) 类型的方法来处理他们。我写了这段代码,但对结果感到很困惑:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace LambdaTest
{
    class LambdaCreator
    {
        ParameterExpression[] Parameters;
        int Index = 0;
        public ParameterExpression Next() 
        {
            return Parameters[Index++];
        }
        public void ResetIndex() 
        {
            Index = 0;
        }

        public void Bar(List<object> parameters)
        {
            foreach (var p in parameters)
            {
                PrintType(p);
            }
        }

        public void PrintType(object arg) 
        {
            Console.WriteLine(arg.GetType().Name);
        }

        public T CreateLambda<T>() where T : class
        {
            var barMethod = GetType().GetMethod("Bar");

            Parameters = typeof(T).GetMethod("Invoke")
                .GetParameters()
                .Select(x => Expression.Parameter(x.ParameterType))
                .ToArray();

            var parametersCount = Expression.Constant(Parameters.Length);

            var listType = typeof(List<object>);
            var list = Expression.Variable(listType);
            var index = Expression.Variable(typeof(int));

            var thisObject = Expression.Constant(this);

            var resetIndex = GetType().GetMethod("ResetIndex");
            var next = GetType().GetMethod("Next");
            var printType = GetType().GetMethod("PrintType");

            var add = listType.GetMethod("Add");

            var breakLabel = Expression.Label();

            var block = Expression.Block(
                new ParameterExpression[] { list, index },
                Expression.Call(thisObject, printType, Parameters.FirstOrDefault()),
                Expression.Call(thisObject, resetIndex),
                Expression.Assign(list, Expression.New(listType)),
                Expression.Loop(
                    Expression.Block(
                        Expression.IfThen(Expression.GreaterThanOrEqual(index, parametersCount), Expression.Break(breakLabel)),
                        Expression.Call(list, add, Expression.Call(thisObject, next)),
                        Expression.AddAssign(index, Expression.Constant(1))
                    ),
                    breakLabel
                ),
                Expression.Call(thisObject, barMethod, list)
            );
            
            var lambda = Expression.Lambda(typeof(T), block, Parameters);
            var compiledLambda = lambda.Compile() as T;
            return compiledLambda;
        }
    }
    class Program
    {
        delegate void Foo(string a, int b);

        static void Main(string[] args)
        {
            var test = new LambdaCreator();
            var l = test.CreateLambda<Foo>();
            l("one", 2);
        }
    }
}

程序的输出是:

String
PrimitiveParameterExpression`1
PrimitiveParameterExpression`1

我期待得到:

String
String
Int32

当我将参数放入列表并将其传递给 Bar 方法时,不知何故丢失了参数的值。 有人可以告诉我问题出在哪里我该如何解决。还是有另一种方法来收集参数并传递它们?我对这种表达树的东西真的很陌生。提前致谢!

发生这种情况是因为这个调用:

Expression.Call(thisObject, printType, Parameters.FirstOrDefault())

实际上编译成这样的:

this.PrintType(a) 

其中 a 是您的委托参数,而此:

Expression.Call(list, add, Expression.Call(thisObject, next))

被编译成类似的东西:

this.PrintType(this.Next()) 

其中一个选项是修改打印方法:

public void PrintType(object arg)
{
    if(arg is ParameterExpression expr)
    {
        Console.WriteLine(expr.Type.Name);
    }
    else
    {
        Console.WriteLine(arg.GetType().Name);
    }
}

要填充列表,您只需创建一个相应的表达式即可:

    var list = Expression.Variable(listType);
    var exprs = new List<Expression>
    {
        Expression.Call(thisObject, resetIndex),
        Expression.Assign(list, Expression.New(listType)),
    };
    
    for (int i = 0; i < @params.Length; i++)
    {
        var ex = Expression.Call(list, add, Expression.Convert(@params[i], typeof(object)));
        exprs.Add(ex);
    }
    
    exprs.Add(Expression.Call(thisObject, barMethod, list));

    var block = Expression.Block(new[] {list}, exprs);

或使用var property = Expression.PropertyOrField(thisObject, nameof(Parameters));(将Parameters更改为List<object>,为其分配新列表并删除块参数)而不是list

您可以在构造 lambda 函数时使用 Parameters 数组在 Expression.Loop 块之前创建一个 NewArrayExpression,然后修改调用代码以访问该数组,如下所示:

// Declare a paramArray parameter to use inside the Expression.Block
var paramArray = Expression.Parameter(typeof(object[]), "paramArray");

var block = Expression.Block(
    new ParameterExpression[] { list, index, paramArray },  // pass in paramArray here
    Expression.Call(thisObject, printType, Parameters.FirstOrDefault()),
    Expression.Call(thisObject, resetIndex),
    Expression.Assign(list, Expression.New(listType)),

    /* Assign the array - make sure to box value types using Expression.Convert */
    Expression.Assign(
        paramArray,
        Expression.NewArrayInit(
            typeof(object),
            Parameters.Select(p => Expression.Convert(p, typeof(object))))),

    Expression.Loop(
        Expression.Block(
            Expression.IfThen(Expression.GreaterThanOrEqual(index, parametersCount), Expression.Break(breakLabel)),
            //Expression.Call(list, add, Expression.Call(thisObject, next)),
            Expression.Call(list, add, Expression.ArrayIndex(paramArray, index)),  // use the paramArray here
            Expression.AddAssign(index, Expression.Constant(1))
        ),
        breakLabel
    ),
    Expression.Call(thisObject, barMethod, list)
);

其余部分没有变化 - 此代码完全替换了 var block = ... 语句。按照您指定的方式工作。