目录
CorrectedConstantAssignmentSample
ConstantAssignmentSampleWithPrintingResult
LoopForCopyingArrayValuesSample
BlockSampleWithLocalVariableAndLineByLineCode
BlockLoopSumUpNumbersFromToSample
介绍
最有用但同时文档记录不佳的C#内置库之一是System.Linq.Expressions。它允许创建复杂的C#代码,将其动态编译为lambda表达式,然后以编译的代码速度运行这些lambda表达式——比使用System.Reflection库实现相同的速度快很多倍。
表达式编码与编写通常的C#代码非常不同,需要剧烈的上下文切换。
我一直在时不时地使用表达式,不是经常,每次使用表达式时,我都必须花一些时间来记住表达式编码的基础知识。
本文的目的是提供信息和示例,使我和其他人将来更容易切换到表达式编码的上下文。
本文的代码示例位于Github上NP.Samples/Expressions的NP.Samples存储库的Expressions文件夹下。
简单表达式与块表达式
简单表达式是那些没有Expression.Block(...)方法构建的表达式。此类表达式更简单,通常足以满足基本目的,但具有以下限制:
- 组合non-block(简单)表达式是通过将更简单的表达式与Expression类中的static方法(Expression.Block(...)方法除外)组合来构建的。这种递归表达式构建会产生所谓的表达式树。生成的代码本质上是通过递归地将更简单的表达式组合替换为更复杂的表达式而获得的单行代码。没有办法正确模仿C#中常见的逐行代码。
- 无法为生成的lambda表达式定义和初始化局部变量。您将在下面的示例中看到,我们必须为non-block表达式创建具有基本上不需要的参数的lambda。
- 如果不使用Block表达式,就无法创建包含一些ref或out参数的对方法的调用的表达式,正是因为只有局部变量可以用作ref或out参数。
简单(non-Block)表达式
大多数内置表达式功能来自System.Linq.Expressions包中的Expression类static方法。
基元表达式或内置表达式是使用Expression类的static工厂方法生成的表达式,例如:
- Expression.Constant(5, typeof(int))将为int类型的常量5创建一个表达式。
- Expression.Parameter(typeof(double), "var1"))将为名为"var1"的double类型的变量创建一个表达式。
就其本身而言,原始(内置)表达式在很大程度上是无用的,但是Expression类还提供了将它们组合成组合表达式并将组合表达式编译为有用的lambda的static方法。
打开控制台应用程序SimpleExpressionsExamples/NP.Samples.Expressions.SimpleExpressionsExamples.sln。
查看主文件Program.cs。文件的内容由static方法组成;每个对应于一个样本。在文件末尾,注释掉的方法调用的编写顺序与定义它们的顺序相同。使用示例时,取消注释相应的方法并运行它;完成后,为了清楚起见,请将其注释掉。
请注意,每个表达式的DebugView属性仅在调试器中可见,该调试器显示表达式的代码并不完全是C#,但对于C#开发人员来说非常相似且易于理解。此属性可以并且应该用于检查和调试表达式。
最简单的表达式示例
在本小节中,我们将介绍不带return语句和循环的简单表达式示例。
WrongConstantAssignmentSample
第一个示例方法称为WrongConstantAssignmentSample()。它的名称以“Wrong”开头,因为它会引发异常。
该方法演示如何将常量分配给变量:
static void WrongConstantAssignmentSample()
{
// Create variable 'myVar' of type 'int'
var paramExpression = Expression.Parameter(typeof(int), "myVar");
// create constant 5
var constExpression = Expression.Constant(5, typeof(int));
// assign constant to the variable
Expression<Action<int>> lambdaExpr =
Expression.Lambda<Action<int>>
(
assignExpression, // lambda body expression
paramExpression // lambda input parameter expression
);
// create lambda expression
Expression<Action> lambdaExpr =
Expression.Lambda<Action>(assignExpression);
// compile lambda expression
Action lambda = lambdaExpr.Compile();
// run lambda
lambda();
}
取消注释并运行Program.cs文件底部的方法调用后,您将看到它在lambdaExpr.Compile()行中抛出异常,异常消息为:
System.InvalidOperationException: 'variable 'myVar' of type 'System.Int32'
referenced from scope '', but it is not defined'
进入调试器中WrongConstantAssignmentSample()的作用域,在调试器中打开lambdaExpr变量并查看其DebugView属性的内容:
.Lambda #Lambda1<System.Action>() {
$myVar = 5
}
如上所述,DebugView属性(仅在调试器中可用)包含表达式代码的详细视图(不完全是C#,但非常接近),可用于调试和修复它。
人们可以立即看到上面的表达式有什么问题——myVar变量从未定义过。
除了我们将在后面描述的Block表达式之外,没有办法在表达式中定义局部变量。
相反,我们可以为lambda定义一个参数,并将其用作lambda表达式中的变量,如下所示。
CorrectedConstantAssignmentSample
看看CorrectedConstantAssignmentSample()方法:
static void CorrectedConstantAssignmentSample()
{
// Create variable 'myVar' of type 'int'
var paramExpression = Expression.Parameter(typeof(int), "myVar");
// create constant 5
var constExpression = Expression.Constant(5, typeof(int));
// assign constant to the variable
var assignExpression = Expression.Assign(paramExpression, constExpression);
// assign constant to the variable
Expression<Action<int>> lambdaExpr =
Expression.Lambda<Action<int>>
(
assignExpression, // lambda body expression
paramExpression // lambda input parameter expression
);
///.Lambda #Lambda1<System.Action`1[System.Int32]>(System.Int32 $myVar) {
/// $myVar = 5
///}
// compile lambda expression
Action<int> lambda = lambdaExpr.Compile();
// run lambda (pass any int number to it)
lambda(0);
}
请注意,结果paramExpression使用了两次:
- 作为作业的左侧部分:Expression.Assign(paramExpression, constExpression);
在C#代码中,这将是myVar = 5; - 作为lambda表达式的输入参数:Expression.Lambda<Action<int>>(assignExpression, <b>paramExpression</b>);
在C#中,这看起来像Lambda(myVar);
看看我们如何获得lambda表达式lambdaExpr:
// create lambda expression (now it has an input parameter)
Expression<Action<int>> lambdaExpr =
Expression.Lambda<Action<int>>
(assignExpression/*body expression*/, paramExpression /* input arg expression */);
我们使用Action<...>作为Expression的Type参数。其Action<...>中的类型数量应等于lambda的输入参数数量(在此示例中——它只是一个整数参数,因此我们有Action<int>)。
我们传递给Expression.Lambda<Action<int>>(...)的参数是:lambda主体表达式,后跟输入参数表达式,其顺序与参数传递给lambda的顺序相同。当然,输入参数表达式的数量应该与类型参数Action<...>的数量相同,并且它们的类型应该匹配。
在此示例中——方法主体由assignExpression表示,而单个整数输入参数由 paramExpression表示。
下面是此示例的lambda表达式DebugView代码:
.Lambda #Lambda1<System.Action`1[System.Int32]>(System.Int32 $myVar) {
$myVar = 5
}
重要说明:如上所述,我们不能在不使用Block表达式的情况下定义局部变量,因此,myVar变量被定义为lambda表达式的输入参数(参数),即使从C#的角度来看——输入参数在其他方面完全无用:它不能在方法之外产生任何变化,因为它是按值传递的。定义局部变量是使用Block表达式的原因之一——这将在下面详细介绍。
我们不再得到例外,但没有迹象表明分配确实发生了。程序在不生成任何控制台输出的情况下运行。
ConstantAssignmentSampleWithPrintingResult
在下一个示例中,我们将演示调用Console.WriteLine(int i)方法来打印变量的结果值。
static void ConstantAssignmentSampleWithPrintingResult()
{
// Create variable 'myVar' of type 'int'
var paramExpression = Expression.Parameter(typeof(int), "myVar");
// create constant 5
var constExpression = Expression.Constant(5, typeof(int));
// assign constant to the variable
var assignExpression = Expression.Assign(paramExpression, constExpression);
// get method info for a Console.WriteLine(int i) method
MethodInfo writeLineMethodInfo =
typeof(Console).GetMethod(nameof(Console.WriteLine), new Type[] {typeof(int)})!;
// we create an expression to call Console.WriteLine(int i)
var callExpression = Expression.Call(writeLineMethodInfo, assignExpression);
// create lambda expression (now it has an input parameter)
Expression<Action<int>> lambdaExpr =
Expression.Lambda<Action<int>>
(
callExpression, /* lambda body expression */
paramExpression /* input parameter expression */
);
///.Lambda #Lambda1<System.Action`1[System.Int32]>(System.Int32 $myVar) {
/// .Call System.Console.WriteLine($myVar = 5)
///}
// compile lambda expression
Action<int> lambda = lambdaExpr.Compile();
// run lambda (pass any int number to it)
lambda(0);
}
以下是我们如何添加调用Console.WriteLine(int i)以打印以下assignExpression结果:
// get method info for a Console.WriteLine(int i) method
MethodInfo writeLineMethodInfo =
typeof(Console).GetMethod(nameof(Console.WriteLine), new Type[] {typeof(int)})!;
// we create an expression to call Console.WriteLine(int i)
var callExpression = Expression.Call(writeLineMethodInfo, assignExpression);
运行此ConstantAssignmentSampleWithPrintingResult()方法会将数字5打印到控制台。
Lambda表达式的DebugView是:
.Lambda #Lambda1<System.Action`1[System.Int32]>(System.Int32 $myVar) {
.Call System.Console.WriteLine($myVar = 5)
}
构建non-Block表达式的原则
上面的示例说明了从简单表达式构建复杂表达式的原则——将较简单的表达式作为参数传递给一些Expression静态方法。
在最后一个示例中,我们使用Expression.Call(...)方法构建一个callExpression表达式并传递给它assignExpression,而该方法又通过处理另外两个表达式的Expression.Assign(...)方法获得——paramExpression和constExpression。简而言之,我们有以下用于调用表达式的表达式树(这是我们的Lambda表达式的主体):
表达式树生成的lambda以递归方式将父表达式的结果传递给子表达式定义的转换。
除了块表达式之外的每个表达式本质上都是一行——尽管这样的行(是组合和扩展其他表达式的结果)可能很长。
具有返回值的Non-Block表达式
表达式(简单和Block)可以返回值(与非void方法相同)。编译这样的表达式将导致Func<...>而不是Action<...> lambda。
您唯一需要做的不同事情是创建一个返回值的lambda表达式,即传递Func<...>而不是Action<...>给Expression.Lambda作为lambda类型参数。与往常一样,Func<...>最后一个类型参数应指定返回类型。
当然,还有一个条件是lambda主体表达式返回一些值,但大多数简单的表达式都会这样做。
在本小节中,我们将提供此类方法的示例。
SimpleReturnConstantSample
看看SimpleReturnConstantSample() static方法:
static void SimpleReturnConstantSample()
{
// create a lambda expression returning integer 1234
var lambdaExpr = Expression.Lambda<Func<int>>
(Expression.Constant(1234, typeof(int)));
///.Lambda #Lambda1<System.Func`1[System.Int32]>() {
/// 1234
///}
// compile lambda expression
var lambda = lambdaExpr.Compile();
// lambda returns 1234
int returnedNumber = lambda();
// 1234 is printed to console
Console.WriteLine(returnedNumber);
}
运行它将打印1234到控制台。
ReturnSumSample
ReturnSumSample() static方法将传递给它的两个整数输入参数相加并返回结果:
static void ReturnSumSample()
{
// integer input parameter i1
var i1Expr = Expression.Parameter(typeof(int), "i1");
// integer input parameter i2
var i2Expr = Expression.Parameter(typeof(int), "i2");
// sum up two numbers expression
var sumExpr = Expression.Add(i1Expr, i2Expr);
//$i1 + $i2
// lambda expression that sums up two numbers and returns the result
var sumLambdaExpr =
Expression.Lambda<Func<int, int, int>>
(
sumExpr, // lambda body expression
i1Expr, // first int parameter i1 expression
i2Expr // second int parameter i2 expression
);
///.Lambda #Lambda1<System.Func`3[System.Int32,System.Int32,System.Int32]>(
/// System.Int32 $i1,
/// System.Int32 $i2)
///{
/// $i1 + $i2
///}
// compile lambda expression
var sumLambda = sumLambdaExpr.Compile();
int i1 = 1, i2 = 2;
// run lambda (i1 + i2)
int result = sumLambda(i1, i2);
// print the result
Console.WriteLine($"{i1} + {i2} = {result}");
}
上面代码中最有趣的部分是:
- 由Expression.Add(param1Expr, param2Expr)方法创建的新简单表达式
- 创建lambda表达式:
// lambda expression that sums up two numbers and returns the result
var sumLambdaExpr =
Expression.Lambda<Func<int, int, int>>
(
sumExpr, // lambda body expression
i1Expr, // first int parameter i1 expression
i2Expr // second int parameter i2 expression
);
请注意,我们使用Func<int, int, int>和Action<int, int>不作为泛型类型参数给Expression.Lambda。
带循环的Non-Block表达式
循环允许使用循环控制流创建表达式。
LoopSample
看看LoopSample():
static void LoopSample()
{
// loop index
var loopIdxExpr = Expression.Parameter(typeof(int), "i");
// max loop index plus 1
var loopIdxToBreakOnExpr = Expression.Parameter(typeof(int), "loopIdxToBreakOn");
// label with return type int will be returned when loop breaks.
LabelTarget breakLabel = Expression.Label(typeof(int), "breakLoop");
// loop expression
var loopExpression =
Expression.Loop
(
// if then else expression
Expression.IfThenElse(
Expression.LessThan(loopIdxExpr,
loopIdxToBreakOnExpr), // if (i < loopIdxToBreakOn)
Expression.PostIncrementAssign(loopIdxExpr), // i++;
Expression.Break(breakLabel, loopIdxExpr) // else return i;
),
breakLabel
);
//.Loop
//{
// .If($i < $loopIdxToBreakOn) {
// $i++
// } .Else {
// .Break #Label1 { $i }
// }
//}
//.LabelTarget #Label1:
var lambdaExpr = Expression.Lambda<Func<int, int, int>>
(
loopExpression, // loop lambda expression body
loopIdxExpr, // loop index (we cannot define it as a local variable,
// so, instead we pass it as an input arg)
loopIdxToBreakOnExpr // input arg expression specifying the number
// to break on when loop index reaches it.
);
//.Lambda #Lambda1<System.Func`3[System.Int32,System.Int32,System.Int32]>(
// System.Int32 $i,
// System.Int32 $loopIdxToBreakOn)
//{
// .Loop {
// .If($i < $loopIdxToBreakOn) {
// $i++
// } .Else {
// .Break #breakLoop { $i }
// }
// }
// .LabelTarget #breakLoop:
//}
var lambda = lambdaExpr.Compile();
int result = lambda(0, 5);
// should print 5
Console.WriteLine(result);
}
简单循环从0迭代到5,然后返回5。返回结果将打印到控制台。该循环实质上模仿以下C#代码:
while(true)
{
if (i < loopIdxToBreakOn)
i++;
else
break;
}
return i;
请注意,i变量不是由循环定义的。这是一个常见问题——无法在非块表达式中定义局部变量。
由于我们不能将循环索引i定义为局部变量,因此我们必须将其定义为lambda的输入参数(即使没有任何其他需要这样做)。
代码中最有趣的部分是:
// label with return type int will be returned when loop breaks.
LabelTarget breakLabel = Expression.Label(typeof(int), "breakLoop");
// loop expression
var loopExpression =
Expression.Loop
(
// if then else expression
Expression.IfThenElse(
Expression.LessThan(loopIdxExpr,
loopIdxToBreakOnExpr), // if (i < loopIdxToBreakOn)
Expression.PostIncrementAssign(loopIdxExpr), // i++;
Expression.Break(breakLabel, loopIdxExpr) // else return i;
),
breakLabel
);
Expression.Label允许创建一个'goto'标签,该标签将指定从循环中断后代码执行恢复的位置。它可以是void,也可以是返回类型(在我们的例子中是int)。
Expression.IfThenElse(...)组合了其他三个表达式——指定循环条件、循环操作以及不再满足循环条件时发生的情况。在我们的例子中,它会产生大约以下代码:
if (i < loopIdxToBreakOn) // loop condition
{
i++; // loop action
}
else
{
return i; // return i if loop condition is not satisfied
}
LoopForCopyingArrayValuesSample
我们的下一个示例LoopForCopyingArrayValuesSample()演示如何创建一个表达式以将一个int[]数组的内容复制到另一个数组中。数组(sourceArray和targetArraty)作为输入参数传递给lambda。假定数组是非null数组并且大小相同,但为了简单起见,我们不在表达式lambda中检查它。
static void LoopForCopyingArrayValuesSample()
{
// assume that the passed arrays are of the same length
// source array expression
var sourceArrayExpr = Expression.Parameter(typeof(int[]), "sourceArray");
// target array expression
var targetArrayExpr = Expression.Parameter(typeof(int[]), "targetArray");
// array cell index (we have to pass it as an input arg
// since there are no local variables)
var arrayCellIdxExpr = Expression.Parameter(typeof(int), "i");
// source array length
var arrayLengthExpr =
Expression.ArrayLength(sourceArrayExpr); // sourceArray.Length
// we do not specify the label type, so loopLabel is void
var loopLabel = Expression.Label("breakLabel");
var loopExpr =
Expression.Loop
(
Expression.IfThenElse
(
Expression.LessThan(arrayCellIdxExpr,
arrayLengthExpr), // if (i < sourceArray.Length)
Expression.Assign(
Expression.ArrayAccess(targetArrayExpr,
arrayCellIdxExpr), // targetArray[i] =
Expression.ArrayAccess(sourceArrayExpr,
Expression.PostIncrementAssign
(arrayCellIdxExpr)) //sourceArray[i++];
),
Expression.Break(loopLabel) // else break;
),
loopLabel
);
//.Loop
//{
// .If($i < $sourceArray.Length) {
// $targetArray[$i] = $sourceArray[$i++]
// } .Else {
// .Break #breakLabel { }
// }
//}
//.LabelTarget #breakLabel:
// unnecessary lambda parameter - arrayCellIdxExpr since we cannot define
// and instantiate a local variable without Block expression
var arrayCopyLambdaExpr =
Expression.Lambda<Action<int[], int[], int>>
(loopExpr, sourceArrayExpr, targetArrayExpr, arrayCellIdxExpr);
//.Lambda #Lambda1<System.Action`3[System.Int32[],System.Int32[],System.Int32]>(
// System.Int32[] $sourceArray,
// System.Int32[] $targetArray,
// System.Int32 $i)
//{
// .Loop
// {
// .If($i < $sourceArray.Length) {
// $targetArray[$i] = $sourceArray[$i++]
// } .Else {
// .Break #breakLabel { }
// }
// }
// .LabelTarget #breakLabel:
//}
var arrayCopyLambda = arrayCopyLambdaExpr.Compile();
int[] sourceArray = Enumerable.Range(1, 10).ToArray();
int[] targetArray = new int[10];
arrayCopyLambda(sourceArray, targetArray, 0);
// will print: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Console.WriteLine(string.Join(", ", targetArray));
}
该方法会将targetArray(从1到10的数字)的内容打印到控制台。
请注意,与非块表达式一样,我们将循环索引变量i作为参数传递,因为我们无法将其定义为局部变量。
代码中最有趣(新)的部分是循环中IfThenElse表达式中的Assign表达式:
Expression.Assign(
Expression.ArrayAccess(targetArrayExpr, arrayCellIdxExpr), // targetArray[i] =
Expression.ArrayAccess(sourceArrayExpr,
Expression.PostIncrementAssign(arrayCellIdxExpr)) // sourceArray[i++];
),
它显示了使用ArrayAccess表达式访问数组单元格。下面是生成的C#代码
targetArray[i] = sourceArray[i++];
请注意,我们被迫在数组索引运算符[]中递增i,因为如果没有Block表达式,我们不能有多个行if语句正文。
LoopSumUpNumbersFromToSample
最后一个非块循环示例将展示如何对to整数变量指定的从0到值的结果整数求和:
static void LoopSumUpNumbersFromToSample()
{
// loop index expression
var loopIdxExpr = Expression.Parameter(typeof(int), "i");
// max index to iterate to
var toExpr = Expression.Parameter(typeof(int), "to");
// result
var resultExpr = Expression.Parameter(typeof(int), "result");
// of type int returns the integer result
var loopLabel = Expression.Label(typeof(int), "breakLabel");
var loopExpr =
Expression.Loop
(
Expression.IfThenElse
(
Expression.LessThanOrEqual(loopIdxExpr, toExpr),// if (i < to)
Expression.AddAssign(resultExpr,
Expression.PostIncrementAssign(loopIdxExpr)), // result += i++;
Expression.Break(loopLabel, resultExpr) // else break the
// loop and return result
),
loopLabel
);
//.Loop
//{
// .If($i <= $to) {
// $result += $i++
// } .Else {
// .Break #breakLabel { $result }
// }
//}
//.LabelTarget #breakLabel:
// unnecessary lambda parameter - resultExpr since we cannot define
// and instantiate a local variable without Block expression
var sumNumbersFromTooLambdaExpr = Expression.Lambda<Func<int, int, int, int>>
(loopExpr, loopIdxExpr, toExpr, resultExpr);
//.Lambda #Lambda1<System.Func`4[System.Int32,System.Int32,System.Int32,
// System.Int32]>(
// System.Int32 $i,
// System.Int32 $to,
// System.Int32 $result)
//{
// .Loop {
// .If($i <= $to) {
// $result += $i++
// }
// .Else {
// .Break #breakLabel { $result }
// }
// }
// .LabelTarget #breakLabel:
//}
var sumNumbersFromTooLambda = sumNumbersFromTooLambdaExpr.Compile();
int from = 1;
int to = 10;
var sumResult = sumNumbersFromTooLambda(from, to, 0);
// prints 'Sum of integers from 1 to 10 is 55'
Console.WriteLine($"Sum of intergers from {from} to {to} is {sumResult}");
}
代码的“有趣”部分是循环的主体:
Expression.AddAssign(resultExpr,
Expression.PostIncrementAssign(loopIdxExpr)), // result += i++;
这将生成以下C#代码:
result += i++;
请注意,由于我们无法定义和初始化局部变量,因此我们必须将变量result的初始值0作为输入参数传递给lambda。
使用Expression.Block方法的表达式
如本文开头所述,具有块方法(或块表达式)的表达式比简单(非块)表达式更强大。特别是,他们
- 允许逐行语句。
- 允许定义不需要作为输入参数传递的局部变量。
- 可以使用ref或out参数包装对方法的调用。
块表达式示例的所有代码都位于 BlockExpressionsExamples/NP 的程序.cs文件中。Samples.Expressions.BlockExpressionsExamples.sln solution.
Program.cs文件的内容由每个static方法对应于单个样本组成。在文件末尾,注释掉的方法调用的编写顺序与定义它们的顺序相同。使用示例时,取消注释相应的方法并运行它;完成后,为了清楚起见,请将其注释掉。
BlockSampleWithLocalVariableAndLineByLineCode
// the block expression below will
// return the following code (i1 + i2)^2
static void BlockSampleWithLocalVariableAndLineByLineCode()
{
var i1Expr = Expression.Parameter(typeof(int), "i1");
var i2Expr = Expression.Parameter(typeof(int), "i2");
// local variable 'result' expression
var resultExpr = Expression.Parameter(typeof(int), "result");
var blockExpr =
Expression.Block
(
typeof(int), // return type of the block
// (skip parameter if void)
new ParameterExpression[] { resultExpr }, // local variable(s)
Expression.Assign(resultExpr,
Expression.Add(i1Expr, i2Expr)), // result = i1 + i2;
// line 1 of block expr
Expression.MultiplyAssign
(resultExpr, resultExpr), // result *= result;
// line 2
resultExpr // return result;
// line 3
);
var lambdaExpr = Expression.Lambda<Func<int, int, int>>
(
blockExpr, // lambda body expression
i1Expr, // lambda input arg 1
i2Expr // lambda input arg 2
);
var lambda = lambdaExpr.Compile();
int i1 = 1, i2 = 2;
var result = lambda(i1, i2); // (1 + 2)^2 = 9
// should print (1 + 2) * (1 + 2) = 9
Console.WriteLine($"({i1} + {i2}) * ({i1} + {i2}) = {result} ");
}
生成的lambda的C#代码大约为:
(int i1, int i2) =>
{
int result;
result = i1 + i2;
result *= result;
return result;
}
当然,没有多行且没有局部变量的非块表达式也可以实现相同的效果,结果如下:
(int i1, int i2) => (i1 + i2) * (i1 + i2)
因此,我们仅介绍此示例来演示块表达式功能。
该方法的“酷”新部分是:
var blockExpr =
Expression.Block
(
typeof(int), // return type of the block
// (skip parameter if void)
new ParameterExpression[] { resultExpr }, // local variable(s)
Expression.Assign(resultExpr,
Expression.Add(i1Expr, i2Expr)), // result = i1 + i2;
// line 1 of block expr
Expression.MultiplyAssign
(resultExpr, resultExpr), // result *= result;
// line 2
resultExpr // return result;
// line 3
);
让我们仔细看看上面使用的Expression.Block(...)方法。
Block(...)的第一个参数指定块的返回类型(在我们的例子中是int)。
第二个参数是指定块的局部变量的ParameterExpression对象数组。在我们的例子中,只有一个int类型的局部变量result。
之后,有对应于代码行的表达式。
重要说明:将返回最后一行表达式(除非是void)——在我们的例子中,返回result变量,因为resultExpr是代码的最后一行。
BlockLoopSumUpNumbersFromToSample
我们的下一个示例类似于上面解释的LoopSumUpNumbersFromToSample() non-Block示例。它产生带有from和to整数输入参数的Lambda,它将从from到to的所有整数相加,包括两个端点。
与非块表达式不同:
- 它不需要将结果作为外部输入参数传递(结果定义为局部变量)
- 它允许初始化结果并在单独的行上递增循环索引。
这是代码:
static void BlockLoopSumUpNumbersFromToSample()
{
// loop index expression
var loopIdxExpr = Expression.Parameter(typeof(int), "i");
// max index to iterate to
var toExpr = Expression.Parameter(typeof(int), "to");
// result
var resultExpr = Expression.Parameter(typeof(int), "result");
// of type int returns the integer result
var loopLabel = Expression.Label(typeof(int), "breakLabel");
var blockExpr =
Expression.Block
(
typeof(int), // returns int
new ParameterExpression[] { resultExpr }, // resultExpr is the
// local variable expression
Expression.Assign
(resultExpr, Expression.Constant(0)), // result = 0;
// initialize result
Expression.Loop
(
Expression.IfThenElse
(
Expression.LessThanOrEqual(loopIdxExpr, toExpr), // if (i < to) {
Expression.Block // block containing multiple lines of expressions
// (it is more clearer
// when i++ is on a separate line)
(
Expression.AddAssign
(resultExpr, loopIdxExpr), // result += i;
Expression.PostIncrementAssign
(loopIdxExpr) // i++;
),
Expression.Break(loopLabel, resultExpr) // } else break the loop
// and return result
),
loopLabel
)
);
// unnecessary lambda parameter - resultExpr since we cannot define
// and instantiate a local variable without Block expression
var sumNumbersFromTooLambdaExpr = Expression.Lambda<Func<int, int, int>>
(blockExpr, loopIdxExpr, toExpr);
var sumNumbersFromTooLambda = sumNumbersFromTooLambdaExpr.Compile();
int from = 1;
int to = 10;
var sumResult = sumNumbersFromTooLambda(from, to);
// prints 'Sum of integers from 1 to 10 is 55'
Console.WriteLine($"Sum of intergers from {from} to {to} is {sumResult}");
}
有趣的部分是:
var blockExpr =
Expression.Block
(
typeof(int), // returns int
new ParameterExpression[] { resultExpr }, // resultExpr is the
// local variable
// expression
Expression.Assign(resultExpr, Expression.Constant(0)), // result = 0;
// initialize result
Expression.Loop
(
Expression.IfThenElse
(
Expression.LessThanOrEqual(loopIdxExpr, toExpr), // if (i < to) {
Expression.Block // block containing multiple lines of expressions
// (it is more clearer when i++ is on a separate line)
(
Expression.AddAssign(resultExpr, loopIdxExpr), // result += i;
Expression.PostIncrementAssign(loopIdxExpr) // i++;
),
Expression.Break(loopLabel, resultExpr) // } else break
// the loop and
// return result
),
loopLabel
)
);
Expression.Block(...)使用两次。顶层Expression.Block(...)定义resultExpr为局部变量,Expression.Assign(resultExpr, Expression.Constant(0))将其初始化为零调用循环并返回循环结果。
循环中的Block表达式表示循环主体,用于将i++增量运算符放置在单独的行result += i;后。在C#中,为了清楚起见,我更喜欢它,即使它将编译为相同的代码。
运行示例方法将导致:
Sum of integers from 1 to 10 is 55
打印到控制台。
BlockCallPlusRefSample
最后一个块示例将演示如何使用块表达式调用带有ref参数的方法。
要调用的方法为:
public static void PlusRef(ref int i1, int i2)
{
i1 += i2;
}
第一个参数i1是ref并用i1和i2的总和进行更新。
此示例的目的是创建一个lambda,将此方法包装在接近以下内容的内容中:
(int i1, int i2) =>
{
int result = i1;
PlusRef(ref result, i2);
return result;
}
请注意,lambda不能ref和out参数,因此我们被迫将输入作为简单int参数传递并返回int。变量result必须作为ref int参数传递给PlusRef(...)方法,因此它必须是局部变量(因为它不能作为ref参数传递给lambda)。
为了能够声明和初始化局部变量,我们被迫使用Block表达式。
static void BlockCallPlusRefSample()
{
// i1 argument expression
var i1Expr = Expression.Parameter(typeof(int), "i1");
// i2 argument expression
var i2Expr = Expression.Parameter(typeof(int), "i2");
// local variable 'result' expression
var resultExpr = Expression.Parameter(typeof(int), "result");
Type plusRefMethodContainer = typeof(Program);
// PlusRef(...) MethodInfo
MethodInfo plusRefMethodInfo =
plusRefMethodContainer.GetMethod(nameof(PlusRef))!;
// block expression
var blockExpr = Expression.Block
(
typeof(int), // block return type
new ParameterExpression[] { resultExpr }, // int result;
// local variable
Expression.Assign(resultExpr, i1Expr), // result = i1;
Expression.Call(plusRefMethodInfo, resultExpr, i2Expr), // call PlusRef
// (ref result, i2)
resultExpr // return result;
);
var lambdaExpr =
Expression.Lambda<Func<int, int, int>>
(
blockExpr, // lambda body expression
i1Expr, // i1 parameter expression
i2Expr // i2 parameter expression
);
var lambda = lambdaExpr.Compile();
int i1 = 1, i2 = 2;
int result = lambda(i1, i2);
Console.WriteLine($"{i1} + {i2} = {result}");
// prints 1 + 2 = 3 to console
}
结论
在本文中,我解释了基础知识并给出了许多编程System.Linq.Expressions的实践示例,介绍了非块和块表达式,解释了表达式的概念,并介绍了Expression类中用于构建各种表达式的众多static方法。
https://www.codeproject.com/Articles/5345657/Csharp-Linq-Expressions-in-Easy-Samples