欢迎光临散文网 会员登陆 & 注册

第 94 讲:C# 3 之 Lambda 表达式

2022-03-13 15:45 作者:SunnieShine  | 我要投稿

欢迎来到 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 不支持全参数弃元

在匿名函数里有一个隐藏技能:全参数弃元。如果你在定义匿名函数的时候,参数一个都不使用,但匿名函数用作匹配类型的时候,必须又得包含参数的时候,声明就可能长这样:

按照基本的书写规则,如果在大括号里,这三个变量都不使用,是可以不写的:

这个现象叫全参数弃元。而 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 个单位的结果,因此整个表达式执行的结果应该是 1%20%5Ctimes%204%20%5Ctimes%207%20%5Ctimes%2010%20%3D%20280。实际上,运行后结果也确实是 280。

当然,这个方法也有别的妙用。比如我们可以拿来拼接字符串序列,来实现 string.Join 差不多的操作:

这样我们可以拼接整个 s 数组序列,得到结果 "Hello, world!"

捕获变量的机制可能在原理上比较难理解,但实际上你掌握了原理之后,也就那么回事:

  1. 捕获变量会把原始数值拿下来一份到闭包里使用;

  2. 尽量不要捕获迭代变量,否则迭代变量捕获起来的结果是循环执行完毕后的结果,而不是当前结果;

  3. 捕获变量的数值不要在方法内去改变,否则改动的也只是拷贝下来的副本而已,除非它是引用类型的实例。

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 表达式的语法,你来看看它们都执行了一些什么,都表示了一些什么。

  1. () => { }

  2. (int x) => ++x

  3. (int x, int y) => x + y

  4. (int x, int y, int z) => { y *= z; return x *= y; }

  5. (int w) => (int x, int y, int z) => w + x + y + z

  6. x => x

  7. x => x.ToString()

  8. (object x, int y) => x == y

  9. (x, y) => string.Format("{0}{1}", x, y)

  10. _ => { }


第 94 讲:C# 3 之 Lambda 表达式的评论 (共 条)

分享到微博请遵守国家法律