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

Lambda 表达式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.23/5 (9投票s)

2015 年 9 月 24 日

CPOL

5分钟阅读

viewsIcon

24002

将 Lambda 表达式表示为委托和表达式树。

引言

Lambda 表达式是一个没有名称的方法,它 具有最少的语法,您可以使用它来创建

  • 委托实例。正如我们所知,委托是一种类型,可以安全地封装一个方法,类似于 C 和 C++ 中的函数指针。它在 C# 中被广泛使用。
  • 表达式树,类型为 Expression<TDelegate>。树代表 Lambda 表达式本身的代码。这允许在运行时分析和解释 Lambda 表达式。

在接下来的段落中,我们将尝试解释这两种选择之间的区别,并解释如何从 Lambda 表达式构建委托实例或表达式树。

Lambda 表达式作为委托

Lambda 表达式可以被理解为匿名方法的演进。它们实际上是匿名方法的超集,因此适用于匿名方法的所有限制也适用于 Lambda 表达式。巨大的好处是它们几乎总是更具可读性且更短。基本上,您可以在 Lambda 表达式中执行(几乎)任何您可以在普通方法体中执行的操作。但有一些限制

  • 您不能使用 breakgotocontinue 来跳出 Lambda 表达式的范围。
  • 不能在 Lambda 表达式中执行不安全的代码。
  • 您不能使用逆变。您必须指定参数类型。

表达式的参数可以是显式类型或隐式类型的。但 Lambda 的返回类型必须隐式可转换为委托的返回类型。Lambda 表达式有很多快捷方式,这使得代码非常紧凑。它们具有特殊的转换规则。

简而言之,让我们看看编译器在编译时如何将 Lambda 表达式翻译。这些行基本上代表了编译器一步一步所做的工作。

// Declare simple function returning length of string.
Func<string, int> f = text => text.Length;

// 1. step - Adding parentheses. Because for one parameter they are optional.
Func<string, int> f = (text) => text.Length;

// 2. step - From left side compiler can easily guess type of input parameter.
Func<string, int> f = (string text) => text.Length;

// 3. step - Adding another parentheses and checking whether type of return value corresponds left side.
Func<string, int> f = (string text) => { return text.Length; };

// 4. step - Now is actually very easy convert expression into delegate.
Func<string, int> f = delegate (string text) { return text.Length; };

我们应该再次强调,这些步骤发生在编译时(而不是运行时)。您可以将其理解为提高代码可读性和紧凑性的语法糖。

我们可以认为编译器的工作到此结束。但事实并非如此。编译器仍在生成 IL 代码。但是编译器会在现有类中创建一个方法。然后,当创建委托实例时,就会使用此方法——就像它是一个普通方法一样。

所以基本上,这段简单的代码

namespace LambdaExpressions
{
    class Program
    {
        static void Main(string[] args)
        {
            Func<string, int> f = text => text.Length;
        }
    }
}

会被转换成这种 IL 形式

这些方法有 C# 中无效的名称,但在 IL 中是有效的。它阻止了在 C# 代码中直接引用它们,并且还避免了命名冲突的可能性。

首先需要注意的一件非常重要的事情是,我们的 Lambda 表达式被创建了一个内部方法。在 IL 中没有“Lambda 表达式方法”这样的概念,而是创建了一个包含您代码的新方法。

第二件需要注意的事情是初始化此委托的奇怪机制。这是 Microsoft 编译器的聪明之处。因为如果代码以后再次调用,此委托就已经被缓存并准备就绪。因此,如果您需要在应用程序中缓存委托,实际上不必这样做。Microsoft 编译器会免费为您完成工作。

表达式树

C# 3 提供了对将 Lambda 表达式转换为表达式树的原生支持。但在此之前,我们需要了解表达式树究竟是什么。

表达式树以树状数据结构表示代码,其中每个节点本身都是一个表达式。这提供了一种表示某些代码的抽象方式。如果您想在执行代码之前修改或转换代码,这可能非常有价值。例如,表达式树用于动态语言运行时 (DLR) 以提供动态语言和 .NET Framework 之间的互操作性或执行 LINQ 查询。

有不同类型的表达式,它们代表可以执行的不同操作。例如,用于加法、比较、求反、条件、方法调用、循环等的表达式。我们不需要一一介绍;完整的列表可以在 MSDN 上找到。

System.Linq.Expressions 命名空间中,可以找到许多类、接口和枚举。最重要的是 Expression 类,它实际上是一个抽象类。它主要包含静态工厂方法来创建表达式类的实例。

让我们从创建一个非常简单的表达式树开始,它将告诉我们一个数字是否为负数。

// Create input parameter expression.
ParameterExpression numberParameter = Expression.Parameter(typeof(int), "n");

// Constant representing 0 value.
ConstantExpression zero = Expression.Constant(0, typeof(int));

// Now create node consisting of input parameter and 0 value.
BinaryExpression numberLessThanZero = Expression.LessThan(numberParameter, zero);

// Define expression and input parameters.
Expression<Func<int, bool>> isNegativeExpression = Expression.Lambda<Func<int, bool>>(
        numberLessThanZero,
        new ParameterExpression[] { numberParameter });

// Before using it we need to compile just written code into delegate instance.
Func<int, bool> compiled = isNegativeExpression.Compile();

// Now we can finally test newly created method.
Console.WriteLine(compiled(-1));    // True
Console.WriteLine(compiled(0));     // False
Console.WriteLine(compiled(1));     // False

这是一个非常简单的例子。通过调用重写的方法 ToString() 生成人类可读的输出。结果完全符合我们的预期。

正如您从示例中看到的,这是创建非常简单的树表达式的痛苦方式。幸运的是,编译器可以帮助我们。解决这个问题的答案是 Lambda 表达式,我们将在下一段中看到。

在我们结束这个简短的描述之前,我们必须提到一个非常重要的事实——表达式是不可变的。一旦创建了某个表达式,它就永远不会改变,因此您可以缓存它并稍后重用。

Lambda 表达式作为表达式树

正如我们已经看到的,Lambda 表达式可以转换为委托实例。第二个可用的转换是转换为表达式树。编译器可以在执行时构建 Expression<TDelegate>。让我们看一个例子

Expression<Func<int, bool>> isNegativeExpression = n => n < 0;

就是这样,不多。与前一段中的示例进行比较。这种表示法简洁易读。我们可以像以前一样完成相同的示例。

Expression<Func<int, bool>> isNegativeExpression = n => n < 0;

Func<int, bool> compiled = isNegativeExpression.Compile();

// Now we can finally test newly created method.
Console.WriteLine(compiled(-1));    // True
Console.WriteLine(compiled(0));     // False
Console.WriteLine(compiled(1));     // False

第一行我们从 Lambda 表达式创建了一个表达式树。下一行我们将其编译为一个委托实例。此编译发生在运行时(而不是编译时)。然后,在其余行中,我们调用此委托进行测试。

请注意,并非所有 Lambda 表达式都可以转换为表达式树。有几个限制。但最重要的是,您不能将带有语句块的 Lambda 表达式转换为表达式树。甚至一个 return 语句也不行。此限制适用于所有 .NET 版本。

参考文献

历史

首次发布 v1.0: 2015 年 9 月 24 日

© . All rights reserved.