探索 C# 中的 Lambda 表达式






4.61/5 (75投票s)
本文涵盖了 C# 中 Lambda 表达式从语法到约束和实现细节的各个方面。
引言
Lambda 表达式是 C# 3.0 语言引入的一种内联委托。它是一种表示匿名方法的简洁方式,提供了一种创建和调用函数的语法。尽管 Lambda 表达式比匿名方法使用起来更简单,但它们在实现方式上略有不同。匿名方法和 Lambda 表达式都允许你内联定义方法的实现,然而,匿名方法明确要求你定义方法的参数类型和返回类型。Lambda 表达式则利用了 C# 3.0 的类型推断特性,允许编译器根据上下文推断变量的类型。
Lambda 表达式可以分解为参数和紧随其后的执行代码。例如:
Parameter => executioncode.
左侧表示零个或多个参数,其后是 Lambda 符号 =>
,用于将参数声明与方法实现分开。Lambda 符号后面是语句体。
Lambda 表达式允许我们将函数作为参数传递给方法调用。我将从一个简单的 Lambda 表达式示例开始,该示例从一个整数列表中返回偶数。
//simple example of lambda expression.
public static void SimpleLambdExpression()
{
List<int> numbers = new List<int>{1,2,3,4,5,6,7};
var evens = numbers.FindAll(n => n % 2 == 0);
var evens2 = numbers.FindAll((int n) => { return n % 2 == 0; });
ObjectDumper.Write(evens);
ObjectDumper.Write(evens2);
}
观察赋给 evens
变量的第一个 Lambda 表达式,你会注意到与匿名方法有几处不同。首先,我们在代码中没有使用 delegate
关键字。其次,我们没有定义参数和返回类型,因为编译器会根据上下文推断类型。表达式中的类型由 delegate
定义确定。因此,在这种情况下,FindAll
方法指定的返回类型接受一个 delegate
,该委托接受一个 int
参数并返回布尔值。不带花括号和返回类型的 Lambda 表达式,提供了表示匿名方法最简洁的方式。如果参数数量为一个,你可以省略参数周围的括号,如第一个 Lambda 表达式所示。尽管 Lambda 表达式不要求显式参数,但你也可以选择定义参数、花括号和返回类型,如赋给 even2
变量的第二个 Lambda 表达式所示。请注意,我们使用了显式的 int
参数,并且像在普通方法中一样指定了返回类型。如果你没有用括号将执行代码括起来,考虑到你正在完整地限定一个方法的所有属性,那么 return 语句将无法工作。
在 Lambda 表达式中需要使用括号的另一种情况是,当你想在 Lambda 表达式内部的多个代码块中使用一个参数时,如下所示:
delegate void WriteMultipleStatements(int i);
public static void MultipleStatementsInLamdas()
{
WriteMultipleStatements write = i =>
{
Console.WriteLine("Number " + i.ToString());
Console.WriteLine("Number " + i.ToString());
};
write(1);
}
在上面的代码示例中,我们用花括号将代码括起来,这样我们就可以在两个表达式中都使用该参数。如果没有花括号,编译器将无法识别变量 i
。
你可以在委托可能没有任何参数的情况下使用 Lambda 表达式。在这种情况下,你必须提供一对空括号来表示一个没有参数的方法。这里有一个简单的例子,说明了不带参数的 Lambda 表达式。
delegate void LambdasNoParams();
public static void LambdasWithNoParameter()
{
LambdasNoParams noparams = () => Console.WriteLine("hello");
noparams();
}
C# 3.0 定义了许多泛型委托,你可以将你的 Lambda 表达式赋给它们,而不是使用能推断类型的 var
关键字。让我们看一个使用其中几个泛型委托的例子:
public static void GenericDelegates()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7 };
Func<int, bool> where = n => n < 6;
Func<int, int> select = n => n;
Func<int, string> orderby = n => n % 2 == 0 ? "even" : "odd";
var nums = numbers.Where(where).OrderBy(orderby).Select(select);
ObjectDumper.Write(nums);
}
在上面的例子中,我们使用了三个不同的扩展方法:where
、orderby
和 select
。where
扩展方法接受一个泛型 delegate
,它带有一个 int
参数和一个布尔类型的返回值,以确定某个元素是否应包含在输出序列中。select
扩展方法接受一个整数参数并返回一个整数,但它也可以返回任何你希望结果转换成的类型——在发送到输出序列之前。在 orderby
扩展方法中,我们接受整数参数并用它来判断是偶数还是奇数,并基于此对结果进行排序。如果我们需要为这三个 Lambda 表达式分别定义三个不同的 delegate
,那将会非常麻烦。随着 C# 3.0 中泛型 delegate
的引入,将我们的 Lambda 表达式赋给泛型 delegate
并将这些 delegate
传递给扩展方法变得相当简单。泛型 delegate
非常方便,可以帮助你避免编写在 .NET 1.1 和 .NET 2.0 中很常见的通用 delegate
(因为当时没有开箱即用的泛型 delegate
)。泛型 delegate
允许你定义最多 4 个参数和 1 个返回类型,所以你可以有一个看起来像这样的 delegate
:
Func<int, bool, string, double, decimal> test;
如果你的方法或 delegate
不满足这些标准,那么你必须手动声明一个接受这些参数的 delegate
。泛型 delegate
通常能覆盖大多数场景,但如果不能满足你的需求,可以随时编写一个自定义的 delegate
。
在某些情况下,类型推断可能不会返回你真正希望 Lambda 表达式返回的数据类型。在这些情况下,我们可以显式地在 Lambda 表达式上指定参数类型。例如:
Func<double, int> expr = (x) => x / 2;
上面的表达式会返回一个编译器错误,因为除以一个 double
后,推断出的类型实际上是 double
。然而,你正在将这个 Lambda 表达式赋给一个返回类型为 int
的 delegate
。如果你真的想让方法返回 int
,那么最好将表达式主体强制转换为 int
以表明你的意图,如下所示:
Func<double, int> expr = (x) => (int)x / 2;
Lambda 表达式基本上有两种类型。一种被认为是简单表达式,其中所有内容都是推断出来的,并且只包含一个表达式。第二种类型的 Lambda 表达式是语句块,它由花括号和返回类型组成。让我们用这两种形式编写一个 Lambda 表达式来看看区别:
//example showing two types of lambda expressions
public static void ExplicitParametersInLambdaExpression()
{
Func<int, int> square = x => x * x;
Func<int, int> square1 = (x) => { return x * x; };
Expression<Func<int, int>> squareexpr = x => x * x;
Expression<Func<int, int>> square2 = (int x) => { return x * x; };//does not compile.
}
让我们逐一剖析每个 Lambda 表达式。第一个 Lambda 表达式被认为是一个简单表达式,它没有语句体,因为没有 return 语句和花括号;而第二个 Lambda 语句则包含一个语句体,因为它有 return 语句和花括号。虽然两者都会被编译成委托,但没有语句体的 Lambda 表达式的好处是它可以被转换成表达式树,特定的提供程序(provider)可以使用它来生成自己的实现。这很像 LINQ to SQL 会将表达式树转换为其领域特定语言 SQL,然后发送到数据库。第三个 Lambda 表达式是 Lambda 表达式与匿名方法真正区别开来的地方。这个语句的妙处在于你可以轻松地将其转换为表达式,而匿名方法只能转换为 delegate
。同样很棒的是,表达式可以通过编译表达式为 delegate
的方式转换回 delegate
,语法如下:
Func<int,int> sq = squareexpr.Compile();
最后一个 Lambda 表达式会引发异常,因为编译器无法转换包含语句体的 Lambda 表达式,正如你所观察到的,它被花括号和 return 语句包围。
虽然你可以使用 Lambda 表达式来生成表达式树,但没有什么能阻止你直接创建自己的表达式树。让我们通过一个例子来为 Lambda 表达式 square = x => x * x
创建一个表达式树。
//example creates expression tree of x *x
public static void CreatingExpressionTree()
{
ParameterExpression parameter1 = Expression.Parameter(typeof(int), "x");
BinaryExpression multiply = Expression.Multiply(parameter1, parameter1);
Expression<Func<int, int>> square = Expression.Lambda<Func<int, int>>(
multiply, parameter1);
Func<int, int> lambda = square.Compile();
Console.WriteLine(lambda(5));
}
你首先需要一个 int
类型的参数表达式。
ParameterExpression parameter1 = Expression.Parameter(typeof(int), "x");
下一步是构建 Lambda 表达式的主体,它恰好是一个二元表达式。该主体包含一个乘法运算符,作用于同一个参数表达式。
BinaryExpression multiply = Expression.Multiply(parameter1, parameter1);
最后一步是构建 Lambda 表达式,它将主体与参数结合起来,如下所示:
Expression<Func<int, int>> square = Expression.Lambda<Func<int, int>>(multiply,
parameter1);
最后一步将表达式转换为 delegate
并执行该 delegate
,如下所示:
Func<int, int> lambda = square.Compile();
Console.WriteLine(lambda(5));
从另一个表达式创建表达式
你可以获取一个表达式树并修改它以创建另一个表达式。在下面的例子中,我们将从一个 x * x
的 Lambda 表达式开始,然后修改这个表达式给它加上 2
。让我们看一个例子:
public static void CreatingAnExpressionFromAnotherExpression()
{
Expression<Func<int, int>> square = x => x * x;
BinaryExpression squareplus2 = Expression.Add(square.Body,
Expression.Constant(2));
Expression<Func<int, int>> expr = Expression.Lambda<Func<int, int>>(squareplus2,
square.Parameters);
Func<int, int> compile = expr.Compile();
Console.WriteLine(compile(10));
}
我们从一个返回 square
(平方)的 Lambda 表达式开始:
Expression<Func<int, int>> square = x => x * x;
接下来,我们通过使用第一个 Lambda 表达式的主体并给它加上一个常量 2
来生成新 Lambda 表达式的主体,并将其赋给二元表达式:
BinaryExpression squareplus2 = Expression.Add(square.Body, Expression.Constant(2));
在最后一步,我们通过将主体与第一个 Lambda 表达式的参数结合来生成新的 Lambda 表达式。我在下面的语句中发现重要的一点是,参数的引用需要与第一个 Lambda 表达式(即 square.Parameters
)的完全相同。你不能创建一个新的 parameters
集合实例,否则会导致运行时错误。
Expression<Func<int, int>> expr = Expression.Lambda<Func<int, int>>(squareplus2,
square.Parameters);
闭包与 Lambda 表达式
闭包(Closure)是一个源于函数式编程的概念。它本质上是捕获或使用在 Lambda 表达式作用域之外的变量。这基本上意味着你可以在 Lambda 表达式内部使用在 Lambda 表达式作用域之外声明的变量——你能够使用并捕获 Lambda 表达式作用域之外的变量。这有其优点,但也可能导致问题,因为外部上下文有能力改变变量的值。让我们深入一个在闭包上下文中使用 Lambda 表达式的例子。
public static void LambdaWithClosure()
{
int mulitplyby = 2;
Func<int, int> operation = x => x * mulitplyby;
Console.WriteLine(operation(2));
}
在上面的例子中,我们在 Lambda 表达式内部使用了 mulitplyby
变量,尽管它是在表达式作用域之外声明的。这个概念被称为变量捕获。在后台,C# 编译器会获取所有这些被捕获的变量,并将它们放在一个生成的类中。当你使用带有外部变量的 Lambda 表达式时,这些变量不会被垃圾回收器回收,而是被迫保留,直到它们被 Lambda 表达式使用并且该表达式超出作用域。
当你在 Lambda 表达式中使用带有 ref
和 out
关键字的参数时,存在一些限制。当你的变量通过 ref
或 out
关键字传递时,你必须显式指定参数类型,因为编译器无法推断变量的类型。如下例所示:
delegate void OutParameter(out int i);
delegate void RefParameter(ref int i);
public static void GotchasWithLambdas()
{
//example with out parameter int i;
OutParameter something = (out int x) => x = 5;
something(out i);
Console.WriteLine(i);
//example with ref parameter.
int a = 2;
RefParameter test = (ref int x) => x++;
test(ref a);
Console.WriteLine(a);
}
请注意,在上面的代码中,我在 ref
和 out
两种情况下都显式指定了 int
参数类型。如果我省略了参数类型,编译器将会报错。
我遇到的另一个使用 Lambda 的限制是,你不能在 Lambda 表达式的参数类型中使用 params
关键字,无论你是否显式指定了参数的类型。下面的代码无法编译,因为参数定义使用了 params
关键字:
delegate void ParmsParameter(params int[] ints);
public static void LambdaWithParam()
{
ParmsParameter par = (params int[] ints) =>
{
foreach (int i in ints)
{
Console.WriteLine(i);
}
};
}
摘要
在本文中,我介绍了 Lambda 表达式的语法——它如何取代匿名方法。我们还讨论了 Lambda 表达式因类型推断及其能轻松转换为委托或表达式树的能力而与匿名方法有何不同。我们学习了 Lambda 表达式的参数限制,以及如何从头开始编写一个表达式并将其编译为委托,反之亦然。
历史
- 2008年3月10日:文章发布
- 2008年3月12日:文章更新