Lambda表达式入门学习笔记
前言:Lambda 到底是个啥
先把这句话记在心里:
Lambda 就是把一个方法写得更短、更偷懒的一种写法。
它本质上还是一个方法,只是省掉了很多啰嗦的格式,让你把”这件事怎么做”用最精炼的方式表达出来。
打个比方。你在工厂里干活,领导让你”把温度超过 80 度的数据挑出来”。按老办法,你得先起个方法名,再写参数类型,再写返回值类型,折腾半天。而 Lambda 就是让你直接把”超过 80 度”这个条件往那一摆就行了,不用再费劲起名字、写一大堆格式。
它长什么样呢?先看一个最简单的样子,混个脸熟:
1 | x => x + 1 |
这行代码的意思就是:给我一个数 x,我还你 x 加 1 的结果。就这么简单。
现在你可能还是一头雾水,没关系。接下来我们一步步从你最熟悉的东西讲起,你很快就会明白为什么它要长成这样。
第一步:先回忆一下,方法是什么
你在之前的学习里应该已经接触过”方法”了。方法就是一段有名字的代码,你给它起个名字,需要的时候叫它的名字就行。
看一个最简单的加法方法:
1 | using System; |
这个代码你应该能看懂。我们逐行拆开看看一个”方法”由哪些部分组成:
| 部分 | 上面的例子 | 说明 |
|---|---|---|
| 返回值类型 | int | 这个方法算完要还给别人一个什么类型的东西 |
| 方法名 | JiaFa | 你自己起的名字,用来调用 |
| 参数列表 | (int a, int b) | 调用这个方法时,需要塞进来的两个数 |
| 方法体 | return a + b | 这个方法具体干的事 |
写一个方法,要写这么多东西。名字、参数类型、返回值类型,一样都不能少。
那么问题来了:如果我只是想临时算一下”两个数加起来”,就为这点小事,非得单独起个方法名、写个完整方法吗?能不能更省事一点?
答案是可以。但是在说 Lambda 之前,我们还得先认识一个”中间站”,那就是委托。
第二步:方法原来也能被”存”起来
先记住一句关键的话:
在 C# 里,方法本身也可以当成一个”值”,存到变量里,或者传给别的方法当参数。
这就像数字可以存到变量里,文字可以存到变量里一样,方法也能存到变量里。
怎么存呢?需要先定义一个”委托类型”。你可以把委托理解成”一个专门用来装方法的盒子”,这个盒子规定了:能装进来的方法,必须长什么样子(接收什么参数、返回什么类型)。
看代码:
1 | using System; |
关键在这一行:
1 | SuanFa f = JiaFa; |
注意,JiaFa 后面没有括号。如果有括号,是”调用”这个方法;没有括号,是”把这个方法本身拿出来”,交给变量 f 存着。
存好之后,f(10, 20) 就等价于 JiaFa(10, 20),结果都是 30。
到这里,你可能会问:这不就是脱裤子放屁,多此一举吗?我直接调用 JiaFa 不就行了?
别急,委托的真正威力在于”把方法当参数传”。比如我可以写一个方法,它不关心具体怎么算,只负责”调用别人传进来的算法”。这样,我以后想换算法,就不用改这个方法了。
我们继续往前走,很快就能看到 Lambda 的价值。
第三步:把方法写短一点,匿名方法
上一节我们为了把方法存起来,先写了一个完整的 JiaFa 方法,再把它交给委托变量。两步,有点绕。
能不能不单独写 JiaFa,而是”当场”把方法的身体直接写出来?可以,这就要用到”匿名方法”。所谓匿名,就是没有名字的方法。
看代码:
1 | using System; |
注意这段代码和上一段最大的区别:我们不再有单独的 static int JiaFa(...) 方法了,而是用 delegate 这个关键字,直接在一个赋值语句里,把方法的身体写了出来。
写成这样,确实比单独定义一个方法要紧凑了。但是你看,还是有点啰嗦:
delegate(int a, int b)这个开头还是要写- 大括号
{ }还是要写 return还是要写
还能不能再短一点?能,这就是下一步要登场的 Lambda。
第四步:Lambda 闪亮登场
先看三行代码的对比,体会一下”越来越短”的过程:
1 | // 最原始:单独定义一个方法 |
看到了吗?Lambda 就是把匿名方法里最啰嗦的部分都去掉,只留下最核心的东西。
完整的可运行代码:
1 | using System; |
现在我们来拆解 Lambda 的结构。一个 Lambda 由三部分组成:
1 | (a, b) => a + b |
| 部分 | 内容 | 说明 |
|---|---|---|
| 参数列表 | (a, b) | 就是方法要接收的参数,用小括号括起来 |
| 箭头 | => | 读作”走你”,左边是输入,右边是输出 |
| 方法体 | a + b | 具体要干的事,也就是方法的身体 |
那个 => 符号,你可以把它读成”变成”或者”得到”。整个读下来就是:给 a 和 b 这两个参数,得到 a 加 b 的结果。
注意,这里我们没有写参数类型 int,也没有写 return,更没有写大括号。C# 会自己根据委托类型 SuanFa 推断出来:因为 SuanFa 规定了两个参数都是 int,所以 a 和 b 自动就是 int 类型;因为 SuanFa 规定了要返回 int,所以 a + b 的结果自动就是返回值。
这就是 Lambda 最大的好处:让编译器自己去猜那些显而易见的东西,你只管写最关键的那句话。
第五步:Lambda 的几种参数情况
Lambda 的参数,根据数量不同,写法略有不同。我们分三种情况看。
情况一:没有参数
如果方法不需要任何参数,就用一对空的小括号。
1 | using System; |
这里的 () => Console.WriteLine(...),左边空括号表示”我不需要参数”,右边就是一句要做的事。
情况二:只有一个参数
只有一个参数时,小括号可以省略,直接写参数名就行。
1 | using System; |
Func<int, int> 是什么?它的意思是”接收一个 int,返回一个 int”的方法。Func 后面的括号里,前面几个类型是参数类型,最后一个类型是返回值类型。这里只有一个 int 参数,返回也是 int,所以写成 Func<int, int>。
情况三:多个参数
有两个或更多参数时,小括号不能省略,参数之间用逗号隔开。
1 | using System; |
Func<int, int, int> 表示”接收两个 int,返回一个 int”。记住这个规律:最后一个类型是返回值,前面的都是参数。
这里有个小规律可以记一下:
| 参数个数 | 括号怎么写 | 例子 |
|---|---|---|
| 0 个 | 必须写空括号 | () => … |
| 1 个 | 括号可以省略 | x => … |
| 2 个及以上 | 括号不能省略 | (x, y) => … |
第六步:两种身体,表达式和语句块
Lambda 的右边(也就是方法体),有两种写法:表达式体和语句块体。
表达式体
如果方法体就是一句话,可以直接写,不用大括号,也不用 return。
1 | x => x + 1 |
这种写法里,x + 1 算出来的结果,自动就是返回值,你不用写 return。
语句块体
如果方法体需要好几句话,就要用大括号包起来,并且这时候必须自己写 return。
1 | x => |
看一个完整的例子:
1 | using System; |
小结一下:
| 身体写法 | 什么时候用 | 要不要写 return |
|---|---|---|
| 表达式体 x => x + 1 | 方法体只有一句话 | 不用写,自动返回 |
| 语句块体 x => { … } | 方法体有多句话 | 必须自己写 return |
第七步:Lambda 能不能有返回值
能,而且这是 Lambda 最常用的地方。
但这里有个关键点:Lambda 自己的返回值类型,不是它说了算,而是由它要”装进”的委托类型说了算。
我们用现成的几个委托类型来理解。C# 早就帮我们准备好了几种常用的委托,不用自己再写 delegate:
| 委托类型 | 含义 | 用法例子 |
|---|---|---|
| Action | 没有返回值的方法 | Action a = () => Console.WriteLine(“你好”); |
| Func | 有返回值的方法,最后一个类型是返回值 | Func<int, int> f = x => x + 1; |
| Predicate | 返回布尔值的方法,用来做判断 | Predicate |
看一个完整例子,体会这三种委托:
1 | using System; |
有了这些现成的委托,你写 Lambda 就方便多了。大部分情况下,你只需要记住 Func 和 Action 这两个就够用。
第八步:Lambda 真正的主战场,集合操作
前面学的都是”基础动作”,现在到了 Lambda 大显身手的地方了。
在写上位机、做工业数据采集的时候,你经常要和一堆数据打交道。比如传感器采集回来一串温度,你要过滤、要排序、要换算。这些操作,配 Lambda 会写得特别简洁。
先记住一个思想:很多集合操作,本质上是”对每个元素做一件事”或者”挑出满足条件的元素”。这种”规则”,用 Lambda 来描述最自然。
我们来看几个最常用的操作。先准备一份数据:
1 | using System; |
注意,这里多用了两个命名空间:System.Collections.Generic(提供 List)和 System.Linq(提供 Where、Select 这些方法)。
操作一:FindAll 挑出符合条件的
比如把超过 80 度的数据挑出来:
1 | using System; |
这里的 t => t > 80 读作:对每个元素 t,判断它是否大于 80。FindAll 会拿这个规则去检查每一个元素,把满足条件的都挑出来,装进一个新的列表里。
操作二:Where 挑出符合条件的(另一个写法)
Where 和 FindAll 效果类似,也是过滤。区别在于它返回的类型不同,而且需要引入 System.Linq。
1 | using System; |
var 表示”让编译器帮我决定类型”,你不用管它具体是啥类型,反正能遍历、能显示就行。
操作三:OrderBy 排序
把数据从小到大排个序:
1 | using System; |
OrderBy(t => t) 里的 t => t 意思是”按元素本身的大小来排序”。如果换成别的规则,比如 OrderBy(t => -t),排序方式就会变。
操作四:Select 批量转换
比如传感器数据有误差,需要把每个数据都加上 10 度做校准:
1 | using System; |
Select 的意思是”对每一个元素都做一遍转换,把转换结果装成新集合”。
操作五:ForEach 逐个处理
对每个元素做点事(比如打印、写文件、累加):
1 | using System; |
组合实战:一个完整的工业场景
把上面几个操作串起来,模拟一个真实的处理流程:采集温度、过滤出异常、做校准、排序、最后输出。
1 | using System; |
这个例子展示了 Lambda 最漂亮的地方:一个复杂的”过滤加换算加排序”的流程,几行代码就写清楚了,而且每一行读起来都像一句人话。
第九步:Lambda 能”记住”外面的变量
Lambda 有一个很有意思的特性:它可以使用它外面定义的变量,而且不是”抄一份”,是”实时引用”。
看代码:
1 | using System; |
注意最后一步:threshold 从 80 改成了 50,然后 isHot(60) 返回了 True。这说明 Lambda 不是把 threshold 的值”抄死”在定义的那一刻,而是始终跟着外面的 threshold 走,外面变了它也跟着变。
这个特性叫”闭包”,理解到”Lambda 能实时使用外面的变量”这个程度,就足够入门了。
第十步:容易踩的坑
坑一:忘记语句块里要写 return
1 | // 错误:语句块里忘了 return |
记住:加了 { } 大括号,就必须自己写 return;没加大括号(表达式体),就千万别写 return。
坑二:多个参数忘了加括号
1 | // 错误:多个参数必须加括号 |
坑三:返回值类型对不上
1 | // 错误:委托要求返回 int,Lambda 却返回了 bool |
坑四:用了 Where 却没引入 Linq
1 | using System; |
用 Where、Select、OrderBy 这些方法前,一定要在文件最上面加上 using System.Linq;。
总结:一张表回顾所有写法
| 写法 | 代码示例 | 说明 |
|---|---|---|
| 完整方法 | int JiaFa(int a, int b) { return a + b; } | 最啰嗦的原始写法 |
| 匿名方法 | delegate(int a, int b) { return a + b; } | 不用起名,但还是啰嗦 |
| Lambda 表达式体 | (a, b) => a + b | 最简洁,最常用 |
| Lambda 语句块 | (a, b) => { return a + b; } | 多行逻辑时用 |
| 无参数 | () => Console.WriteLine(“你好”) | 空括号 |
| 单参数 | x => x * 2 | 括号可省略 |
| 多参数 | (x, y) => x + y | 括号不能省 |
再记住三个最常用的现成委托:
| 委托 | 作用 | 例子 |
|---|---|---|
| Action | 无返回值 | Action a = () => Console.WriteLine(“你好”); |
| Func | 有返回值,最后一个是返回类型 | Func<int, int> f = x => x + 1; |
| Predicate | 返回真假 | Predicate |
最后,把 Lambda 的本质再默念一遍:
Lambda 就是一个写起来更短的方法。左边小括号里是参数,中间是箭头,右边是要做的事。
练习题
自己动手写一写,写出来才算真的会。
练习一
写一个 Lambda,接收一个整数,返回它的平方。然后用它算出 7 的平方是多少。
练习二
写一个 Lambda,接收两个整数,返回其中较大的那一个。
练习三
有一组数据 List<int> nums = new List<int> { 3, 15, 8, 22, 6, 19 };,请用 Lambda 挑出所有偶数,并从小到大排序输出。
练习四
有一组价格数据 List<int> prices = new List<int> { 12, 35, 8, 100, 27, 50 };,请用 Select 把所有价格统一打八折(乘以 0.8),再挑出打折后仍然大于等于 20 的,最后输出。
练习五
定义一个外部变量 int limit = 10;,写一个 Lambda 判断某个数是否大于 limit。先测一下 15 和 5,然后把 limit 改成 20,再测一次 15,观察结果有什么变化。
建议:把每一个示例代码都亲手敲一遍,运行一下看看输出,再改几个数字试试。Lambda 这东西,看十遍不如写一遍。写熟练了,你以后看别人代码里那些
x => x + 1就不会发怵了。