65.9K
CodeProject 正在变化。 阅读更多。
Home

C# LINQ 表达式的简单示例

starIconstarIconstarIconstarIconstarIcon

5.00/5 (20投票s)

2022年10月30日

MIT

13分钟阅读

viewsIcon

27985

通过简单的例子解释表达式编程的概念。

引言

C# 中最有用的、同时文档又很差的内置库之一是 System.Linq.Expressions。它允许动态地创建复杂的 C# 代码,将其编译成 lambda 表达式,然后以编译后的代码速度运行这些 lambda 表达式——比使用 System.Reflection 库实现相同功能要快得多。

表达式编码与编写常规 C# 代码截然不同,需要进行彻底的上下文切换。

我时不时地接触表达式,但并不频繁,每次接触表达式时,我都必须花一些时间来回忆表达式编码的基础知识。

本文的目的是提供信息和示例,以便将来我本人以及他人能更轻松地进行表达式编码的上下文切换。

本文的代码示例位于 Github 上 NP.Samples 仓库的 Expressions 文件夹下,地址为 NP.Samples/Expressions

简单表达式与块表达式

简单表达式是指不使用 Expression.Block(...) 方法构建的表达式。这类表达式更简单,通常足以满足基本需求,但存在以下限制:

  1. 复合的非块(简单)表达式是通过结合 Expression 类(除了 Expression.Block(...) 方法之外)的 static 方法来构建的。这种递归的表达式构建会产生所谓的表达式树。生成的代码本质上是通过递归地将更简单表达式的组合代入更复杂的表达式而获得的一行代码。无法正确模仿 C# 中常见的逐行代码。
  2. 无法为生成的 lambda 表达式定义和初始化局部变量。我们将在下面的示例中看到,我们不得不为非块表达式创建具有基本不需要的参数的 lambda。
  3. 如果没有使用块表达式,就无法创建一个包装了具有 refout 参数的方法调用的表达式,这正是因为只有局部变量才能用作 refout 参数。

简单(非块)表达式

大部分内置表达式功能来自 System.Linq.Expressions 包中的 Expression 类的 static 方法。

原始或内置表达式是使用 Expression 类的 static 工厂方法构建的表达式,例如:

  • Expression.Constant(5, typeof(int)) 将创建一个类型为 int、值为 5 的常量表达式。
  • Expression.Parameter(typeof(double), "var1")) 将创建一个类型为 double、名为 "var1" 的变量表达式。

原始(内置)表达式本身大部分情况下是无用的,但 Expression 类还提供了 static 方法将它们组合成复合表达式,并将复合表达式编译成有用的 lambda。

打开一个控制台应用程序 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 变量从未被定义。

除了块表达式(我们稍后将介绍)之外,无法在表达式中定义局部变量。

相反,我们可以定义 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 被使用了两次。

  1. 作为赋值的左侧:Expression.Assign(paramExpression, constExpression);
    在 C# 代码中,这相当于 myVar = 5;
  2. 作为 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<...> 用作 ExpressionType 参数。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
} 

重要提示:如上所述,我们不能在不使用块表达式的情况下定义局部变量,因此,myVar 变量被定义为 lambda 表达式的输入参数(即使从 C# 角度来看,该输入参数在其他方面完全无用:由于按值传递,它无法在方法外部产生任何更改。定义局部变量是使用块表达式的原因之一——稍后将详细介绍。

我们不再收到异常,但没有任何迹象表明赋值真的发生了。程序运行而不产生任何控制台输出。

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)
} 

构建非块表达式的原理

上面的示例说明了从简单表达式构建复杂表达式的原理——您将简单表达式作为参数传递给某些 Expressionstatic 方法。

在最后一个示例中,我们通过使用 Expression.Call(...) 方法构建了一个 callExpression 表达式,并向其传递了 assignExpression,而 assignExpression 又是由 Expression.Assign(...) 方法处理另外两个表达式——paramExpressionconstExpression——获得的。简而言之,我们有如下的调用表达式(它是我们 Lambda 表达式的主体)的表达式树:

表达式树会生成 lambda,这些 lambda 会递归地将父表达式的**结果**传递给子表达式定义的转换。

除块表达式外,每个表达式本质上都是一行代码——尽管该行代码(作为组合和展开其他表达式的结果)可能会非常长。

带有返回值(非块)表达式

表达式(无论是简单表达式还是块表达式)都可以被设置为返回一个值(与非 void 方法相同)。编译这样的表达式将生成 Func<...> 而不是 Action<...> lambda。

您需要做的唯一不同的事情是创建返回值的 lambda 表达式,即在 Expression.Lambda 中将 Func<...> 而不是 Action<...> 作为 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}");
}

上面代码中最有趣的部分是:

  1. Expression.Add(param1Expr, param2Expr) 方法创建的新简单表达式。
  2. 创建 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 的泛型类型参数。

带有循环的非块表达式

循环允许创建带有循环控制流的表达式。

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[] 数组的内容复制到另一个数组。数组(sourceArraytargetArraty)作为输入参数传递给 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

最后一个非块循环示例将展示如何将从 0 到由 to 整数变量指定的值的所有连续整数相加。

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 方法的表达式

如本文开头所述,使用 Block 方法的表达式(或块表达式)比简单(非块)表达式功能更强大。特别是,它们:

  1. 允许逐行语句。
  2. 允许定义不需要作为输入参数传递的局部变量。
  3. 可以包装对具有 refout 参数的方法的调用。

块表达式示例的所有代码都位于 BlockExpressionsExamples/NP.Samples.Expressions.BlockExpressionsExamples.sln 解决方案的 Program.cs 文件中。

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() 非块示例类似。它生成一个带有 fromto 整数输入参数的 Lambda,该 Lambda 计算从 fromto(包括两个端点)的所有整数之和。

与非块表达式不同:

  • 它不需要将结果作为外部输入参数传递(结果被定义为局部变量)。
  • 它允许在单独的行上初始化结果和递增循环索引。

这是代码

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)) 将其初始化为零,调用循环并返回循环结果。

循环内的块表达式代表循环主体,用于将 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;
}

第一个参数 i1ref 类型,它将更新为 i1i2 的和。

此示例的目的是创建一个包装了 such 方法的 lambda,接近于:

(int i1, int i2) =>
{
    int result = i1;
    PlusRef(ref result, i2);

    return result;
}

请注意,lambda 不能有 refout 参数,因此我们被迫将输入作为简单的 int 参数传递,并将返回值也作为 int。变量 result 必须作为 ref int 参数传递给 PlusRef(...) 方法,因此它必须是局部变量(因为它不能作为 ref 参数传递给 lambda)。

为了能够声明和初始化局部变量,我们被迫使用块表达式。

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 方法。

历史

  • 2022年10月30日:初始版本
© . All rights reserved.