第 94 讲:C# 3 之 Lambda 表达式
欢迎来到 C# 享有一定地位的语法特性:Lambda 表达式的介绍文章。
Part 1 引例
在前面介绍的内容里,我们学会了如何使用匿名函数来完成一些内联函数语法的机制,来简化代码内容。我们还是使用排序操作来举例。
调用方:
它确实简化了不少的代码。但问题在于,它还不够简单。首先我们要知道,这样的代码是要反复书写 delegate
关键字的,不论什么匿名函数,它必须作为开头写出来,作为编译器识别匿名函数语法的一个“特征”。然后返回语句也只有一句代码(left.CompareTo(right)
这个表达式结果直接拿来返回了)。像是 for
循环,一句代码我们还能省略大括号,可这样的匿名函数语法也确实不够简单。
于是,C# 3 进一步简化匿名函数的语法,创建了一个新的语法特性:Lambda 表达式(Lambda Expression)。
Part 2 语法
Lambda 表达式的语法是这样的:
看着挺复杂的,下面我们来对这些语法来说明一下。
2-1 =>
的概念,以及它前面的参数表列
Lambda 表达式使用了一个新的运算符 =>
来表示和标识 Lambda 表达式语法。这个运算符称为 Lambda 算符(Lambda Operator),读作“执行”(英语环境下则读作“goes to”)。当然,因为它的写法很像是一个胖胖的箭头,所以也有地方称为“胖箭头运算符”;而 C# 原生指针语法里的指针对象间址运算符 ->
则因为前面跟的是减号(单横线)而不是等号(双横线)因此称为“瘦箭头运算符”。当然,这不是重点,它读作“执行”是有一定原因的。
在 =>
符号的左边,它表示了一系列的参数。这些参数在 C# 3 里得到了强化和优化,这使得它们自己的类型甚至都可以不写出来。举个例子,我们从 C# 2 匿名函数的比较函数(前面写的那个)改写为 C# 3 的 Lambda 运算符后,表达式的参数部分就长这样了:
我们先不关心 =>
后面的内容,这个我们稍后再说。你看这个写法,delegate
关键字没了,参数连类型都可以不写了,编译器能够自己推断和决定其类型;我们只需要绑定参数表列的时候使用一堆小括号就可以了。这是不是简化了很多?
当然,如果你要写类型,也是可以的:
(int left, int right) => ...
。另外,如果这个 Lambda 表达式只有一个参数的时候,一旦省略了它的类型名称不写的时候,甚至这对小括号都可以不写。比如(int x) => ...
,在x
省略类型的时候,可以简写为(x) => ...
或甚至是x => ...
。
而 =>
后面,则跟的是表达式或者语句。下面我们来说一下 =>
后面写的东西。
2-2 =>
后的表达式或语句
在 =>
后,跟的是和匿名函数一致的语法:执行语句。不过,C# 3 的 Lambda 表达式对其有所优化。
回忆一下前面讲解匿名类型的递归提过一嘴的说法:lambda 演算。我不是让你回忆全部内容,因为这些内容过于专业了,也不一定每个人都知道和了解它。而这个计算机学科 lambda 演算和这里的 Lambda 表达式是有一定关系的,这也是为什么 Lambda 表达式要叫做“Lambda”表达式的真实原因。
在这个学科里,lambda 演算用到的是形如 \lambda x.\ x 的写法。还记得这个写法吗?这等价于匿名函数的 delegate (int x) { return x; }
。是的,这个写法下,lambda 演算并未声明任何关于 return
之类的关键字,而小数点 .
也仅仅是分隔前面的参数表列和后面的执行表达式。
是的,这种写法在 C# 3 里用起来了。Lambda 表达式里,按道理原本写法应为 (int x) => { return x; }
或者忽略参数类型以及括号 x => { return x; }
,但它仍然不够简单。于是,C# 3 的 Lambda 有一种匿名类型里独特的简写规则:如果 =>
后跟的执行内容只有一句表达式的话,那么这个表达式就作为 =>
后面的部分,进而可以省略大括号。也就是说,{ return x; }
这一坨,我们可以只提取出 x
作为 return
表达式的结果,直接写在 =>
后,并省略其它的内容,进而将这个表达式简化为 x => x
。是的,这个写法相当简单,而且多数时候,=>
后也都只跟一句话,因为大多数使用匿名函数或者是 Lambda 表达式语法的时候,我们要求执行的语句,也就一两句用得最多。而一句的时候,是大量被用到的。因此,C# 3 的机制借鉴了 lambda 演算的写法,允许 Lambda 表达式进行“究极简化”。
因此,我们前面的比较操作的 Lambda 表达式语法,就可以进一步改写为这样:
是的。这样相当好用。但是这里需要你特别注意,一旦使用 Lambda 表达式进行这样的改写后,一句话后的分号 ;
是一定不写的。因为整个 (left, right) => left.CompareTo(right)
是一个表达式。而 =>
后的部分是表达式,前面是参数表列,因此不需要任何的 ;
。
那么,改写后的语法长这样:
是的,这样就相当简单了。
2-3 无参时不可省略小括号,以及无执行语句时不可省略大括号
在 C# 2 的匿名类型语法里,我们介绍了一种极端情况:无参匿名函数。这样的匿名函数在写的时候,delegate () { ... }
的小括号可以省略。但是,在 C# 3 的 Lambda 表达式里,由于语法设计机制,使得这是唯一一个比 C# 2 匿名函数语法复杂的地方:C# 3 的 Lambda 表达式除非参数表列只有一个参数,并且不写类型的时候,才可以省略小括号;否则不论如何都必须给出小括号。因此,你不得不这么写:
与此同时,如果大括号里没有东西的话,大括号也不可省略:
虽然 () => {}
看着挺奇怪的,但是你得知道,()
代表了参数表列没有东西,而 {}
代表了执行内容没有任何语句。因此这样的 Lambda 表达式没有任何执行的内容,也不带任何参数。
2-4 当参数带有修饰符的时候,不可省略类型名
考虑下面的语法:
假设我有第 3 行这样的数据类型 F
,而我在第 1 行为其进行赋值。可问题就在于,这个 Lambda 表达式的两个参数都有 ref
修饰符。这可以省略吗?
答案是,很遗憾,不能。C# 3 的 Lambda 表达式对这个语法做了不少设计上的简化和优化,但带有修饰符的 Lambda 表达式目前还是没有合适的计算和类型推断算法。因此,下面两种简化语法是不对的简化写法:
2-5 Lambda 不支持全参数弃元
在匿名函数里有一个隐藏技能:全参数弃元。如果你在定义匿名函数的时候,参数一个都不使用,但匿名函数用作匹配类型的时候,必须又得包含参数的时候,声明就可能长这样:
按照基本的书写规则,如果在大括号里,这三个变量都不使用,是可以不写的:
Part 3 Lambda 表达式的闭包和变量捕获
很高兴的是,它的捕获机制和 C# 2 的匿名函数是完全一致的,因此你不需要重新学一遍。也就是说,C# 3 好像只换了一下语法,简化了代码,其它的都没有变化过。而且 C# 3 的 Lambda 表达式将其还进行了语法的增进和优化。
我们还是给一个例子在这里吧。假设我们设计了一个 Aggregate
方法,用于按照指定的规则结合整个序列,并返回结合结果的方法:
这里稍微说一下 Func<T, T, T>
。之前说过,Func
委托类型系列的最后一个泛型参数是这个委托类型的返回值的类型,因此这个 Func<T, T, T>
的意思是,给定两个 T
类型的数据当参数,计算得到结果也是 T
类型的。
当然,这个方法的用法很简单:
请观察第 2 行代码。我们可以看到,三个参数一一对应了上面给的三个参数。第一个参数是 source
序列,而第二个参数是初始值,第三个参数则是执行过程,怎么结合两个元素的。注意这里我们使用了捕获机制来捕获了 Lambda 执行表达式外侧的 a
变量,这里用作 a + next
表达式里,用来增大 next
一个单位。
顺带一说。代码里用到的单词 interim 的意思是“临时内容”、“暂定内容”、“期间的内容”之类的意思。在这里就表示“中间量”。是一个名词,也可以当形容词来用。
可以看到,初始数值我们设置为 1,而整个序列都是将中间结果和当前结果相乘,并且每一次执行都是乘以了比当前量大 1 个单位的结果,因此整个表达式执行的结果应该是 。实际上,运行后结果也确实是 280。
当然,这个方法也有别的妙用。比如我们可以拿来拼接字符串序列,来实现 string.Join
差不多的操作:
这样我们可以拼接整个 s
数组序列,得到结果 "Hello, world!"
。
捕获变量的机制可能在原理上比较难理解,但实际上你掌握了原理之后,也就那么回事:
捕获变量会把原始数值拿下来一份到闭包里使用;
尽量不要捕获迭代变量,否则迭代变量捕获起来的结果是循环执行完毕后的结果,而不是当前结果;
捕获变量的数值不要在方法内去改变,否则改动的也只是拷贝下来的副本而已,除非它是引用类型的实例。
Part 4 Lambda 的递归
也不必多说。Lambda 方法的递归和匿名函数的递归版本完全一致,只不过有了一些额外的简化语法,因此代码可能会更少一些。
这一次,我们递归,写起来就简单多了。
注意 f => x => x == 0 ? 1 : x * f(x - 1)
。这个 Lambda 是嵌套的,这个嵌套机制我们需要从内而外看。首先有两个 =>
,因此我们不得不当内层的 x => ...
的 x
是参数,而后面是执行结果;而执行结果里我们使用 x == 0 ? 1 : f * (x - 1)
作为返回结果。注意这里我们在内层 Lambda 表达式里捕获了外部变量 f
(这个 f
是相对于内层 Lambda 的执行表达式来说,是外部的变量,所以也叫外部变量)。最后,将整个 Lambda 表达式作为返回结果,于是 f => ...
的意思就出来了:一个 f
作为参数,并返回一个 Lambda 表达式的 Lambda 表达式。
Part 5 参数名影射
这个和 C# 2 的匿名函数也是完全一样的机制。你可以在内层使用和外部变量一致的名称作为参数名。这个机制和匿名函数的机制也都是一样的,所以不必重复第二遍。
Part 6 总结:匿名函数和 Lambda 表达式的使用选取原则
其实……C# 3 Lambda 表达式基本上完胜 C# 2 的匿名函数,C# 3 的 Lambda 表达式可以做到 C# 2 匿名函数所有的内容,除了那个无参数简化小括号那个,而且 C# 3 的 Lambda 表达式还添加了一些额外的简化规则,比如类型推断规则允许我们省略参数类型,省略单参数无类型书写格式的小括号,以及大括号里只有一个执行语句的时候的大括号。所以,C# 3 在基本所有维度来说都比 C# 2 的匿名函数更好,因此,我建议你在任何时候都使用 Lambda 表达式而不是匿名函数。
这里总结一个表格,列举一下匿名函数和 Lambda 表达式的区别。

匿名函数和 Lambda 表达式原生是不支持递归的,因为它们没有方法名称,因此必须借鉴嵌套语法这个小技巧才能实现递归,而从语法上是并不支持的。
下面我们来看一些 Lambda 表达式的语法,你来看看它们都执行了一些什么,都表示了一些什么。
() => { }
(int x) => ++x
(int x, int y) => x + y
(int x, int y, int z) => { y *= z; return x *= y; }
(int w) => (int x, int y, int z) => w + x + y + z
x => x
x => x.ToString()
(object x, int y) => x == y
(x, y) => string.Format("{0}{1}", x, y)
_ => { }