第 38 节 · 第四部分 进阶特性

匿名方法

匿名方法和 Lambda 都是没有名字的方法,写在用它的地方,当场交给委托;调用委托时写法和有名字的方法一样。

时长 7:15授课计划第 16 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1匿名方法是什么

前面讲委托时,让委托指向一段代码,得先写一个有名字的方法。匿名方法就是没有名字的方法,写在用它的地方,当场交给委托;它和 Lambda 表达式合起来叫匿名函数。

委托规定了方法的签名(教程说的“相同标签”就是签名):参数是什么、返回什么。签名对得上的方法装进委托,调用委托,就是调用装在里面的方法。以前装进委托的是 AddNum 这样有名字的方法,匿名方法则把一段代码块直接当作委托的值交进去。

视频里把委托变量比成读卡座:有名字的方法是一张只刻着方法名的铭牌卡,匿名方法是代码直接印在卡面上的卡。其实编译器会悄悄给匿名方法起个名字,我们写代码时用不到它。

本节的 hidden、lambda、button 几个例子是顶级语句写法(不写类和 Main,代码从第一行开始执行);NumberChanger 在文件末尾声明,是一个接收一个 int、不返回值的委托类型。

C# · 第 1-5 行
1NumberChanger nc = delegate(int x)// NumberChanger:接收一个 int、不返回值的委托类型;nc 装进匿名方法
2{// 匿名方法开始
3 Console.WriteLine("Anonymous Method: {0}", x);// 匿名方法的主体:打出 x
4};// 匿名方法结束,分号结束整条赋值语句
5Console.WriteLine(nc.Method.Name);// nc.Method.Name 取出委托里那个方法的名字
运行结果
<<Main>$>b__0_0

2Lambda 表达式

Lambda 是写匿名函数最简洁的办法,常用在 LINQ(对一组数据做查询、筛选的工具,后面会讲)和委托里。写法是 (参数) => 表达式;要写好几条语句,就在箭头右边加一对大括号:(参数) => { 语句; }。

Func<int, int, int> 是现成的委托:两个 int 进,一个 int 出;a、b 的类型由它推出来是 int。

C# · 第 2-3 行
2Func<int, int, int> add = (a, b) => a + b;// Lambda:a、b 进,返回 a + b
3Console.WriteLine(add(2, 3)); // 输出 5// add(2, 3):2 + 3 = 5

用 Lambda 筛数组:Where 是 LINQ(对一组数据做查询、筛选的工具)提供的筛选方法,把每个数交给 Lambda 判断,余数是 0 的偶数留下来。

C# · 第 6-11 行
6int[] numbers = { 1, 2, 3, 4, 5 };// numbers 里放着 1 到 5
7var evenNumbers = numbers.Where(n => n % 2 == 0);// n 除以 2 的余数等于 0 就留下;var 让编译器自己推出类型
8foreach (var num in evenNumbers)// 逐个取出筛出来的数
9{// foreach 开始
10 Console.WriteLine(num); // 输出 2 4// 每个数占一行
11}// foreach 结束
运行结果
5
2
4

原来的数组一个没少,Where 只是把符合条件的挑了出来。

3用 delegate 写匿名方法

格式是 delegate(参数) { 语句; },用 delegate 关键字当场创建一个委托实例。大括号里的代码是匿名方法的主体,调用写法和普通方法一样:nc(10)。

C# · 第 24-30 行
24NumberChanger nc = delegate(int x)// nc 装进匿名方法,参数 int x 对上 NumberChanger 的签名
25{// 匿名方法开始
26 Console.WriteLine("Anonymous Method: {0}", x);// 主体:打出 Anonymous Method 和 x
27};// 匿名方法结束;分号是整条赋值语句的结尾
28
29// 使用匿名方法调用委托
30nc(10);// 调用委托,x = 10,打出 Anonymous Method: 10

大括号后面的分号是整条语句的结尾,不是方法体自己要的。漏写就报错,编译器以为你还要接着声明下一个变量:

C# · 第 4 行
4}// 匿名方法结束,这里漏了分号
编译报错
Program.cs(4,2): error CS1003: 语法错误,应输入“,”

4匿名方法的两个小例子

delegate 后面的参数要写类型;不用写返回类型,但 return 交出的值要对上 Func 最后那个 int。

C# · 第 2-6 行
2Func<int, int, int> multiply = delegate (int a, int b)// multiply 装进匿名方法,参数写明 int a、int b
3{// 匿名方法开始
4 return a * b;// 返回 int,对上 Func 的返回类型
5};// 匿名方法结束
6Console.WriteLine(multiply(2, 3)); // 输出 6// multiply(2, 3):2 × 3 = 6

把 return 改成交出一个字符串,就转换不成这个委托:

C# · 第 3 行
3return "a * b";// 交出的是 string,和 Func 要的 int 对不上
编译报错
Program.cs(3,12): error CS0029: 无法将类型“string”隐式转换为“int”
Program.cs(3,12): error CS1662: 无法将 匿名方法 转换为预期委托类型,因为块中的某些返回类型不可隐式转换为委托返回类型

第二个例子用匿名方法当按钮的事件处理程序。教程这段直接放进控制台程序会报错,因为 Button 是 Windows 窗体程序里的按钮,控制台程序里没有:

C# · 第 9 行
9Button button = new Button();// 创建按钮对象,控制台程序里找不到 Button
编译报错
Program.cs(9,1): error CS0246: 未能找到类型或命名空间名“Button”(是否缺少 using 指令或程序集引用?)
Program.cs(9,21): error CS0246: 未能找到类型或命名空间名“Button”(是否缺少 using 指令或程序集引用?)

自己补一个最小的 Button:有一个 Click 事件,PerformClick 模拟点一下。处理程序的参数里,sender 是触发事件的对象(这里是按钮),EventArgs 是附带的事件信息。

C# · 第 10-14 行
10button.Click += delegate (object sender, EventArgs e)// 用 += 把匿名方法挂到 Click 事件上
11{// 匿名方法开始
12 Console.WriteLine("Button clicked!");// 被点时打出一行字
13};// 匿名方法结束
14button.PerformClick();// 模拟点一下,挂上的匿名方法被执行
查看完整代码(22 行)
C#
1// 示例:使用匿名方法定义一个委托
2Func<int, int, int> multiply = delegate (int a, int b)// multiply 装进匿名方法,两个 int 进,一个 int 出
3{// 匿名方法开始
4 return a * b;// 返回 a 乘 b
5};// 匿名方法结束,分号结束整条语句
6Console.WriteLine(multiply(2, 3)); // 输出 6// 2 × 3 = 6
7
8// 示例:使用匿名方法作为事件处理程序
9Button button = new Button();// 创建一个按钮对象(用的是下面自己写的 Button)
10button.Click += delegate (object sender, EventArgs e)// 订阅 Click 事件,处理程序是一个匿名方法
11{// 匿名方法开始
12 Console.WriteLine("Button clicked!");// 被点时打出 Button clicked!
13};// 匿名方法结束
14button.PerformClick();// 模拟点击,引发 Click 事件
15
16// 控制台程序里没有窗体的 Button,自己写一个最小的
17delegate void ClickHandler(object sender, EventArgs e);// 声明事件用的委托类型:接收 sender 和 EventArgs,不返回值
18class Button// 定义 Button 类
19{// Button 类开始
20 public event ClickHandler? Click;// Click 事件;? 表示可以没人订阅(为空)
21 public void PerformClick() => Click?.Invoke(this, EventArgs.Empty);// ?.Invoke:有人订阅才调用;这里的 => 不是 Lambda,是一行写完的方法体
22}// Button 类结束
运行结果
6
Button clicked!

5完整例子

TestDelegate 里有静态变量 num,开始是 10;AddNum 给它加上参数,MultNum 给它乘上参数,都会打出新的 num。同一个委托变量 nc 先后装了匿名方法和两个有名字的方法,调用写法完全一样。

C# · 第 33-42 行
33nc = new NumberChanger(AddNum);// nc 换成有名字的方法 AddNum
34
35// 使用命名方法调用委托
36nc(5);// 调用 AddNum(5):num = 10 + 5 = 15
37
38// 使用另一个命名方法实例化委托
39nc = new NumberChanger(MultNum);// nc 再换成 MultNum
40
41// 使用命名方法调用委托
42nc(2);// 调用 MultNum(2):num = 15 × 2 = 30
查看完整代码(46 行)
C#
1using System;// 引入 System 命名空间
2
3delegate void NumberChanger(int n);// 声明委托类型:接收一个 int,不返回值
4namespace DelegateAppl// 定义命名空间 DelegateAppl
5{// 命名空间开始
6 class TestDelegate// 定义 TestDelegate 类
7 {// TestDelegate 类开始
8 static int num = 10;// 静态变量 num,初值 10
9 public static void AddNum(int p)// AddNum:签名和 NumberChanger 对得上
10 {// AddNum 开始
11 num += p;// num 加上参数 p
12 Console.WriteLine("Named Method: {0}", num);// 打出 Named Method 和新的 num
13 }// AddNum 结束
14
15 public static void MultNum(int q)// MultNum:签名同样对得上
16 {// MultNum 开始
17 num *= q;// num 乘上参数 q
18 Console.WriteLine("Named Method: {0}", num);// 打出 Named Method 和新的 num
19 }// MultNum 结束
20
21 static void Main(string[] args)// 程序入口 Main 方法
22 {// Main 开始
23 // 使用匿名方法创建委托实例
24 NumberChanger nc = delegate(int x)// nc 装进匿名方法,参数 int x
25 {// 匿名方法开始
26 Console.WriteLine("Anonymous Method: {0}", x);// 匿名方法的主体:打出 x
27 };// 匿名方法结束,分号结束整条语句
28
29 // 使用匿名方法调用委托
30 nc(10);// 打出 Anonymous Method: 10
31
32 // 使用命名方法实例化委托
33 nc = new NumberChanger(AddNum);// nc 换成 AddNum
34
35 // 使用命名方法调用委托
36 nc(5);// num = 10 + 5 = 15,打出 Named Method: 15
37
38 // 使用另一个命名方法实例化委托
39 nc = new NumberChanger(MultNum);// nc 换成 MultNum
40
41 // 使用命名方法调用委托
42 nc(2);// num = 15 × 2 = 30,打出 Named Method: 30
43 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
44 }// Main 结束
45 }// TestDelegate 类结束
46}// 命名空间结束
运行结果
Anonymous Method: 10
Named Method: 15
Named Method: 30

6改用 Lambda 来写

只有一个参数时括号可以省掉,箭头后面直接写要执行的语句;其余代码都没变。$"...{x}" 是内插字符串,把 x 的值填进大括号的位置。

C# · 第 26 行
26NumberChanger nc = x => Console.WriteLine($"Lambda Expression: {x}");// x => ...:一个参数,省掉括号
运行结果
Lambda Expression: 10
Named Method: 15
Named Method: 30

Lambda 表达式是 C# 3.0 引入的,C# 2.0 引入的是匿名方法。实测时在项目文件(.csproj)里写 <LangVersion>ISO-2</LangVersion>,就是按 C# 2 编译:匿名方法的版本照常运行,输出和上面一样;Lambda 的版本编译报错,另一条是 $ 开头的内插字符串,它到 C# 6 才有:

C# · 第 26 行
26NumberChanger nc = x => Console.WriteLine($"Lambda Expression: {x}");// 按 C# 2 编译这一行:Lambda 和内插字符串都还没有
编译报错
Program.cs(26,34): error CS8023: 功能“lambda 表达式”在 C# 2 中不可用。请使用 3 或更高的语言版本。
Program.cs(26,55): error CS8023: 功能“内插字符串”在 C# 2 中不可用。请使用 6 或更高的语言版本。

7小结

  • 匿名方法和 Lambda 都没有名字,写在用它的地方,直接交给委托。
  • 匿名方法用 delegate 关键字,参数要写类型,大括号后面的分号别漏。
  • Lambda 用箭头 =>,参数类型能省,常用在 LINQ 里筛数据。
  • 不用写返回类型,但 return 交出的值必须和委托对得上。

下一节:C# 能不能像 C 语言那样,用指针直接操作内存?

检查题

5 道,选完马上告诉你对不对。

  1. 1Func<int, int, int> add = (a, b) => a + b; Console.WriteLine(add(2, 3)); 输出?

  2. 2numbers = {1,2,3,4,5},foreach (var num in numbers.Where(n => n % 2 == 0)) Console.WriteLine(num); 输出?

  3. 3NumberChanger nc = delegate(int x) { … } 大括号后面漏写分号?

  4. 4教程完整例子(num 开始是 10,nc 先装匿名方法传 10,再装 AddNum 传 5,再装 MultNum 传 2)三行输出?

  5. 5Lambda 表达式从哪个版本开始有?