Lambda 表达式






4.23/5 (9投票s)
将 Lambda 表达式表示为委托和表达式树。
引言
Lambda 表达式是一个没有名称的方法,它 具有最少的语法,您可以使用它来创建
- 委托实例。正如我们所知,委托是一种类型,可以安全地封装一个方法,类似于 C 和 C++ 中的函数指针。它在 C# 中被广泛使用。
- 表达式树,类型为
Expression<TDelegate>
。树代表 Lambda 表达式本身的代码。这允许在运行时分析和解释 Lambda 表达式。
在接下来的段落中,我们将尝试解释这两种选择之间的区别,并解释如何从 Lambda 表达式构建委托实例或表达式树。
Lambda 表达式作为委托
Lambda 表达式可以被理解为匿名方法的演进。它们实际上是匿名方法的超集,因此适用于匿名方法的所有限制也适用于 Lambda 表达式。巨大的好处是它们几乎总是更具可读性且更短。基本上,您可以在 Lambda 表达式中执行(几乎)任何您可以在普通方法体中执行的操作。但有一些限制
- 您不能使用
break
、goto
或continue
来跳出 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 版本。
参考文献
- C# in Depth, 3rd Edition - Jon Skeet
- C# 5.0 in a Nutshell: The Definitive Reference Fifth Edition Edition - Joseph Albahari, Ben Albahari
- https://msdn.microsoft.com/en-us/library/bb397687.aspx
- https://msdn.microsoft.com/en-us/library/bb397951.aspx
- https://msdn.microsoft.com/en-us/library/ms173172.aspx
- https://msdn.microsoft.com/en-us/library/system.linq.expressions.expression.aspx
- https://msdn.microsoft.com/en-us/library/orm-9780596516109-03-09.aspx
- http://csharpindepth.com/ViewChapterNotes.aspx?Chapter=9&Edition=1&printable=true
- https://codeproject.org.cn/Articles/24255/Exploring-Lambda-Expression-in-C
- https://codeproject.org.cn/Articles/17575/Lambda-Expressions-and-Expression-Trees-An-Introdu
历史
首次发布 v1.0: 2015 年 9 月 24 日