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 = ...
语句。按照您指定的方式工作。
我正在尝试创建一个任意类型的运行时 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 = ...
语句。按照您指定的方式工作。