【问题标题】:C# - Creating lambda functions using expression trees of an arbitrary delegate typeC# - 使用任意委托类型的表达式树创建 lambda 函数
【发布时间】:2020-08-26 23:12:12
【问题描述】:

我正在尝试创建一个任意类型的运行时 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 方法时,不知何故我丢失了它们的值。 有人可以告诉我问题出在哪里,我该如何解决。还是有另一种方法来收集论点并通过它们?我对这种表达树的东西真的很陌生。提前致谢!

【问题讨论】:

    标签: c# linq lambda reflection expression-trees


    【解决方案1】:

    您可以在构造 lambda 函数时,在 Expression.Loop 块之前使用 Parameters 数组创建一个 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 = ... 语句。按照您指定的方式工作。

    【讨论】:

    • 太好了,这正是我想要的!谢谢!
    【解决方案2】:

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

    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&lt;object&gt;,为其分配新列表并删除块参数)而不是list

    【讨论】:

    • 或者直接调用 this.Next/this.Reset 而不将它们另外包装在另一个 MethodCallExpression 中。无论哪种方式,如果 OP 尝试创建多个委托,由于在生成器类中跟踪状态,OP 都会遇到问题。
    • 嗯,我写了PrintType方法,只是举例。我打算以各种方式处理列表,实际上我需要传递给 lambda 的值...
    • @pinkfloydx33 这不起作用,因为Next 只会被调用一次,而不是在循环中。
    • @brick 您需要将您的课程分成两部分 - 一个用于创建 lambda,另一个用于处理状态。否则,您将在多次调用您创建的 lambda 时遇到问题 - 例如在这种情况下应该发生什么:l("one", 2); l("two", 3)?或者如果使用同一个创建者来创建多个 lambda。
    • @GuruStron 是的,来自单个创建者的多个 lambda 是另一个问题。我会考虑一下。但我目前主要关心的是如何将传递给 lambda 的值放入列表中?你有想法吗?有可能吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-05
    • 1970-01-01
    • 1970-01-01
    • 2015-05-14
    • 1970-01-01
    • 2016-04-15
    相关资源
    最近更新 更多