前言:Lambda 到底是个啥

先把这句话记在心里:

Lambda 就是把一个方法写得更短、更偷懒的一种写法。

它本质上还是一个方法,只是省掉了很多啰嗦的格式,让你把”这件事怎么做”用最精炼的方式表达出来。

打个比方。你在工厂里干活,领导让你”把温度超过 80 度的数据挑出来”。按老办法,你得先起个方法名,再写参数类型,再写返回值类型,折腾半天。而 Lambda 就是让你直接把”超过 80 度”这个条件往那一摆就行了,不用再费劲起名字、写一大堆格式。

它长什么样呢?先看一个最简单的样子,混个脸熟:

1
x => x + 1

这行代码的意思就是:给我一个数 x,我还你 x 加 1 的结果。就这么简单。

现在你可能还是一头雾水,没关系。接下来我们一步步从你最熟悉的东西讲起,你很快就会明白为什么它要长成这样。


第一步:先回忆一下,方法是什么

你在之前的学习里应该已经接触过”方法”了。方法就是一段有名字的代码,你给它起个名字,需要的时候叫它的名字就行。

看一个最简单的加法方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;

class Program
{
// 定义一个加法方法,起名叫 JiaFa
static int JiaFa(int a, int b)
{
return a + b;
}

static void Main()
{
int result = JiaFa(3, 5);
Console.WriteLine(result); // 输出 8
}
}

这个代码你应该能看懂。我们逐行拆开看看一个”方法”由哪些部分组成:

部分 上面的例子 说明
返回值类型 int 这个方法算完要还给别人一个什么类型的东西
方法名 JiaFa 你自己起的名字,用来调用
参数列表 (int a, int b) 调用这个方法时,需要塞进来的两个数
方法体 return a + b 这个方法具体干的事

写一个方法,要写这么多东西。名字、参数类型、返回值类型,一样都不能少。

那么问题来了:如果我只是想临时算一下”两个数加起来”,就为这点小事,非得单独起个方法名、写个完整方法吗?能不能更省事一点?

答案是可以。但是在说 Lambda 之前,我们还得先认识一个”中间站”,那就是委托。


第二步:方法原来也能被”存”起来

先记住一句关键的话:

在 C# 里,方法本身也可以当成一个”值”,存到变量里,或者传给别的方法当参数。

这就像数字可以存到变量里,文字可以存到变量里一样,方法也能存到变量里。

怎么存呢?需要先定义一个”委托类型”。你可以把委托理解成”一个专门用来装方法的盒子”,这个盒子规定了:能装进来的方法,必须长什么样子(接收什么参数、返回什么类型)。

看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

class Program
{
// 定义一个加法方法
static int JiaFa(int a, int b)
{
return a + b;
}

// 定义一个委托类型,代表"接收两个 int,返回一个 int"的方法
delegate int SuanFa(int a, int b);

static void Main()
{
// 把 JiaFa 这个方法,存到变量 f 里
SuanFa f = JiaFa;

// 现在通过变量 f 来调用方法
int result = f(10, 20);
Console.WriteLine(result); // 输出 30
}
}

关键在这一行:

1
SuanFa f = JiaFa;

注意,JiaFa 后面没有括号。如果有括号,是”调用”这个方法;没有括号,是”把这个方法本身拿出来”,交给变量 f 存着。

存好之后,f(10, 20) 就等价于 JiaFa(10, 20),结果都是 30。

到这里,你可能会问:这不就是脱裤子放屁,多此一举吗?我直接调用 JiaFa 不就行了?

别急,委托的真正威力在于”把方法当参数传”。比如我可以写一个方法,它不关心具体怎么算,只负责”调用别人传进来的算法”。这样,我以后想换算法,就不用改这个方法了。

我们继续往前走,很快就能看到 Lambda 的价值。


第三步:把方法写短一点,匿名方法

上一节我们为了把方法存起来,先写了一个完整的 JiaFa 方法,再把它交给委托变量。两步,有点绕。

能不能不单独写 JiaFa,而是”当场”把方法的身体直接写出来?可以,这就要用到”匿名方法”。所谓匿名,就是没有名字的方法。

看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;

class Program
{
// 定义一个委托类型
delegate int SuanFa(int a, int b);

static void Main()
{
// 不单独写方法,直接就地写一个"没名字的方法"
SuanFa f = delegate(int a, int b)
{
return a + b;
};

int result = f(100, 200);
Console.WriteLine(result); // 输出 300
}
}

注意这段代码和上一段最大的区别:我们不再有单独的 static int JiaFa(...) 方法了,而是用 delegate 这个关键字,直接在一个赋值语句里,把方法的身体写了出来。

写成这样,确实比单独定义一个方法要紧凑了。但是你看,还是有点啰嗦:

  • delegate(int a, int b) 这个开头还是要写
  • 大括号 { } 还是要写
  • return 还是要写

还能不能再短一点?能,这就是下一步要登场的 Lambda。


第四步:Lambda 闪亮登场

先看三行代码的对比,体会一下”越来越短”的过程:

1
2
3
4
5
6
7
8
9
10
11
// 最原始:单独定义一个方法
static int JiaFa(int a, int b)
{
return a + b;
}

// 匿名方法:不用单独定义,但还是要 delegate 和 return
delegate(int a, int b) { return a + b; }

// Lambda:连 delegate 和 return 都省了
(a, b) => a + b

看到了吗?Lambda 就是把匿名方法里最啰嗦的部分都去掉,只留下最核心的东西。

完整的可运行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;

class Program
{
// 定义一个委托类型
delegate int SuanFa(int a, int b);

static void Main()
{
// Lambda 写法
SuanFa f = (a, b) => a + b;

int result = f(100, 200);
Console.WriteLine(result); // 输出 300
}
}

现在我们来拆解 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
2
3
4
5
6
7
8
9
10
11
12
using System;

class Program
{
static void Main()
{
// Action 表示"没有返回值、没有参数"的方法
Action sayHello = () => Console.WriteLine("你好,世界");

sayHello(); // 输出:你好,世界
}
}

这里的 () => Console.WriteLine(...),左边空括号表示”我不需要参数”,右边就是一句要做的事。

情况二:只有一个参数

只有一个参数时,小括号可以省略,直接写参数名就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

class Program
{
static void Main()
{
// 省略括号的写法
Func<int, int> doubleIt = x => x * 2;

Console.WriteLine(doubleIt(21)); // 输出 42

// 不省略括号,加个括号也完全可以
Func<int, int> doubleIt2 = (x) => x * 2;

Console.WriteLine(doubleIt2(10)); // 输出 20
}
}

Func<int, int> 是什么?它的意思是”接收一个 int,返回一个 int”的方法。Func 后面的括号里,前面几个类型是参数类型,最后一个类型是返回值类型。这里只有一个 int 参数,返回也是 int,所以写成 Func<int, int>

情况三:多个参数

有两个或更多参数时,小括号不能省略,参数之间用逗号隔开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

class Program
{
static void Main()
{
// 两个参数
Func<int, int, int> add = (x, y) => x + y;

Console.WriteLine(add(3, 4)); // 输出 7

// 三个参数
Func<int, int, int, int> addThree = (a, b, c) => a + b + c;

Console.WriteLine(addThree(1, 2, 3)); // 输出 6
}
}

Func<int, int, int> 表示”接收两个 int,返回一个 int”。记住这个规律:最后一个类型是返回值,前面的都是参数。

这里有个小规律可以记一下:

参数个数 括号怎么写 例子
0 个 必须写空括号 () => …
1 个 括号可以省略 x => …
2 个及以上 括号不能省略 (x, y) => …

第六步:两种身体,表达式和语句块

Lambda 的右边(也就是方法体),有两种写法:表达式体和语句块体。

表达式体

如果方法体就是一句话,可以直接写,不用大括号,也不用 return。

1
x => x + 1

这种写法里,x + 1 算出来的结果,自动就是返回值,你不用写 return。

语句块体

如果方法体需要好几句话,就要用大括号包起来,并且这时候必须自己写 return。

1
2
3
4
5
6
x =>
{
int temp = x + 1;
Console.WriteLine("算完了");
return temp;
};

看一个完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;

class Program
{
static void Main()
{
// 表达式体:一行搞定,不用 return
Func<int, int> simple = x => x * 10;
Console.WriteLine(simple(5)); // 输出 50

// 语句块体:多行代码,必须自己写 return
Func<int, int> complex = x =>
{
int result = x * 10;
result = result + 1;
return result;
};
Console.WriteLine(complex(5)); // 输出 51
}
}

小结一下:

身体写法 什么时候用 要不要写 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 p = x => x > 10;

看一个完整例子,体会这三种委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;

class Program
{
static void Main()
{
// Action:没有返回值
Action greet = () => Console.WriteLine("开始处理数据");

// Func:有返回值
Func<int, int, int> add = (a, b) => a + b;

// Predicate:返回真假,用来判断
Predicate<int> isBig = n => n > 100;

greet(); // 输出:开始处理数据
Console.WriteLine(add(2, 3)); // 输出 5
Console.WriteLine(isBig(150)); // 输出 True
Console.WriteLine(isBig(50)); // 输出 False
}
}

有了这些现成的委托,你写 Lambda 就方便多了。大部分情况下,你只需要记住 Func 和 Action 这两个就够用。


第八步:Lambda 真正的主战场,集合操作

前面学的都是”基础动作”,现在到了 Lambda 大显身手的地方了。

在写上位机、做工业数据采集的时候,你经常要和一堆数据打交道。比如传感器采集回来一串温度,你要过滤、要排序、要换算。这些操作,配 Lambda 会写得特别简洁。

先记住一个思想:很多集合操作,本质上是”对每个元素做一件事”或者”挑出满足条件的元素”。这种”规则”,用 Lambda 来描述最自然。

我们来看几个最常用的操作。先准备一份数据:

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
static void Main()
{
// 假设这是传感器采集回来的一组温度数据
List<int> temperatures = new List<int> { 45, 82, 61, 95, 78, 103, 70 };
}
}

注意,这里多用了两个命名空间:System.Collections.Generic(提供 List)和 System.Linq(提供 Where、Select 这些方法)。

操作一:FindAll 挑出符合条件的

比如把超过 80 度的数据挑出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;

class Program
{
static void Main()
{
List<int> temperatures = new List<int> { 45, 82, 61, 95, 78, 103, 70 };

// 挑出所有超过 80 度的数据
List<int> hot = temperatures.FindAll(t => t > 80);

Console.WriteLine("超过80度的有:" + string.Join(", ", hot));
// 输出:超过80度的有:82, 95, 103
}
}

这里的 t => t > 80 读作:对每个元素 t,判断它是否大于 80。FindAll 会拿这个规则去检查每一个元素,把满足条件的都挑出来,装进一个新的列表里。

操作二:Where 挑出符合条件的(另一个写法)

Where 和 FindAll 效果类似,也是过滤。区别在于它返回的类型不同,而且需要引入 System.Linq。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
static void Main()
{
List<int> temperatures = new List<int> { 45, 82, 61, 95, 78, 103, 70 };

var hot = temperatures.Where(t => t > 80);

Console.WriteLine("超过80度的有:" + string.Join(", ", hot));
// 输出:超过80度的有:82, 95, 103
}
}

var 表示”让编译器帮我决定类型”,你不用管它具体是啥类型,反正能遍历、能显示就行。

操作三:OrderBy 排序

把数据从小到大排个序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
static void Main()
{
List<int> temperatures = new List<int> { 45, 82, 61, 95, 78, 103, 70 };

// 从小到大排序
var sorted = temperatures.OrderBy(t => t);

Console.WriteLine("从小到大:" + string.Join(", ", sorted));
// 输出:从小到大:45, 61, 70, 78, 82, 95, 103

// 从大到小排序
var sortedDesc = temperatures.OrderByDescending(t => t);

Console.WriteLine("从大到小:" + string.Join(", ", sortedDesc));
// 输出:从大到小:103, 95, 82, 78, 70, 61, 45
}
}

OrderBy(t => t) 里的 t => t 意思是”按元素本身的大小来排序”。如果换成别的规则,比如 OrderBy(t => -t),排序方式就会变。

操作四:Select 批量转换

比如传感器数据有误差,需要把每个数据都加上 10 度做校准:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
static void Main()
{
List<int> temperatures = new List<int> { 45, 82, 61, 95, 78, 103, 70 };

// 每个数据都加 10,得到校准后的新列表
var calibrated = temperatures.Select(t => t + 10);

Console.WriteLine("校准后:" + string.Join(", ", calibrated));
// 输出:校准后:55, 92, 71, 105, 88, 113, 80
}
}

Select 的意思是”对每一个元素都做一遍转换,把转换结果装成新集合”。

操作五:ForEach 逐个处理

对每个元素做点事(比如打印、写文件、累加):

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
using System.Collections.Generic;

class Program
{
static void Main()
{
List<int> temperatures = new List<int> { 45, 82, 61, 95, 78, 103, 70 };

// 逐个打印
temperatures.ForEach(t => Console.WriteLine("当前温度:" + t + " 度"));
}
}

组合实战:一个完整的工业场景

把上面几个操作串起来,模拟一个真实的处理流程:采集温度、过滤出异常、做校准、排序、最后输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
static void Main()
{
// 原始采集数据
List<int> raw = new List<int> { 45, 82, 61, 95, 78, 103, 70, 88, 59 };

// 第一步:挑出超过 80 度的异常数据
var abnormal = raw.Where(t => t > 80);

// 第二步:给这些数据统一减 5 度,做温度补偿校准
var fixedData = abnormal.Select(t => t - 5);

// 第三步:从大到小排序,方便看最严重的
var result = fixedData.OrderByDescending(t => t);

// 第四步:输出最终结果
Console.WriteLine("需要重点关注的数据(从高到低):");
Console.WriteLine(string.Join(", ", result));
// 输出:98, 90, 83, 77, 83
}
}

这个例子展示了 Lambda 最漂亮的地方:一个复杂的”过滤加换算加排序”的流程,几行代码就写清楚了,而且每一行读起来都像一句人话。


第九步:Lambda 能”记住”外面的变量

Lambda 有一个很有意思的特性:它可以使用它外面定义的变量,而且不是”抄一份”,是”实时引用”。

看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;

class Program
{
static void Main()
{
int threshold = 80; // 外面定义的阈值

// Lambda 里用到了外面的 threshold
Func<int, bool> isHot = t => t > threshold;

Console.WriteLine(isHot(90)); // True
Console.WriteLine(isHot(70)); // False

// 现在改变外面的阈值
threshold = 50;

Console.WriteLine(isHot(60)); // True,因为阈值已经变成 50 了
}
}

注意最后一步:threshold 从 80 改成了 50,然后 isHot(60) 返回了 True。这说明 Lambda 不是把 threshold 的值”抄死”在定义的那一刻,而是始终跟着外面的 threshold 走,外面变了它也跟着变。

这个特性叫”闭包”,理解到”Lambda 能实时使用外面的变量”这个程度,就足够入门了。


第十步:容易踩的坑

坑一:忘记语句块里要写 return

1
2
3
4
5
6
7
8
// 错误:语句块里忘了 return
Func<int, int> f = x => { x + 1; }; // 编译会报错

// 正确:要么用表达式体(不用 return)
Func<int, int> f1 = x => x + 1;

// 正确:要么在语句块里写 return
Func<int, int> f2 = x => { return x + 1; };

记住:加了 { } 大括号,就必须自己写 return;没加大括号(表达式体),就千万别写 return。

坑二:多个参数忘了加括号

1
2
3
4
5
// 错误:多个参数必须加括号
Func<int, int, int> f = x, y => x + y; // 编译报错

// 正确
Func<int, int, int> f = (x, y) => x + y;

坑三:返回值类型对不上

1
2
3
4
5
6
// 错误:委托要求返回 int,Lambda 却返回了 bool
Func<int, int> f = x => x > 10; // 编译报错

// 正确:想返回真假,要用 Predicate 或者 Func<int, bool>
Predicate<int> p = x => x > 10;
Func<int, bool> p2 = x => x > 10;

坑四:用了 Where 却没引入 Linq

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;
// 少了这一行:using System.Linq;

class Program
{
static void Main()
{
List<int> list = new List<int> { 1, 2, 3 };
var result = list.Where(x => x > 1); // 报错:找不到 Where
}
}

用 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 p = x => x > 10;

最后,把 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 就不会发怵了。