1匿名方法是什么
前面讲委托时,让委托指向一段代码,得先写一个有名字的方法。匿名方法就是没有名字的方法,写在用它的地方,当场交给委托;它和 Lambda 表达式合起来叫匿名函数。
委托规定了方法的签名(教程说的“相同标签”就是签名):参数是什么、返回什么。签名对得上的方法装进委托,调用委托,就是调用装在里面的方法。以前装进委托的是 AddNum 这样有名字的方法,匿名方法则把一段代码块直接当作委托的值交进去。
视频里把委托变量比成读卡座:有名字的方法是一张只刻着方法名的铭牌卡,匿名方法是代码直接印在卡面上的卡。其实编译器会悄悄给匿名方法起个名字,我们写代码时用不到它。
本节的 hidden、lambda、button 几个例子是顶级语句写法(不写类和 Main,代码从第一行开始执行);NumberChanger 在文件末尾声明,是一个接收一个 int、不返回值的委托类型。
| 1 | NumberChanger nc = delegate(int x) | // NumberChanger:接收一个 int、不返回值的委托类型;nc 装进匿名方法 |
| 2 | { | // 匿名方法开始 |
| 3 | Console.WriteLine("Anonymous Method: {0}", x); | // 匿名方法的主体:打出 x |
| 4 | }; | // 匿名方法结束,分号结束整条赋值语句 |
| 5 | Console.WriteLine(nc.Method.Name); | // nc.Method.Name 取出委托里那个方法的名字 |
<<Main>$>b__0_0
2Lambda 表达式
Lambda 是写匿名函数最简洁的办法,常用在 LINQ(对一组数据做查询、筛选的工具,后面会讲)和委托里。写法是 (参数) => 表达式;要写好几条语句,就在箭头右边加一对大括号:(参数) => { 语句; }。
Func<int, int, int> 是现成的委托:两个 int 进,一个 int 出;a、b 的类型由它推出来是 int。
| 2 | Func<int, int, int> add = (a, b) => a + b; | // Lambda:a、b 进,返回 a + b |
| 3 | Console.WriteLine(add(2, 3)); // 输出 5 | // add(2, 3):2 + 3 = 5 |
用 Lambda 筛数组:Where 是 LINQ(对一组数据做查询、筛选的工具)提供的筛选方法,把每个数交给 Lambda 判断,余数是 0 的偶数留下来。
| 6 | int[] numbers = { 1, 2, 3, 4, 5 }; | // numbers 里放着 1 到 5 |
| 7 | var evenNumbers = numbers.Where(n => n % 2 == 0); | // n 除以 2 的余数等于 0 就留下;var 让编译器自己推出类型 |
| 8 | foreach (var num in evenNumbers) | // 逐个取出筛出来的数 |
| 9 | { | // foreach 开始 |
| 10 | Console.WriteLine(num); // 输出 2 4 | // 每个数占一行 |
| 11 | } | // foreach 结束 |
5 2 4
原来的数组一个没少,Where 只是把符合条件的挑了出来。
3用 delegate 写匿名方法
格式是 delegate(参数) { 语句; },用 delegate 关键字当场创建一个委托实例。大括号里的代码是匿名方法的主体,调用写法和普通方法一样:nc(。
| 24 | NumberChanger nc = delegate(int x) | // nc 装进匿名方法,参数 int x 对上 NumberChanger 的签名 |
| 25 | { | // 匿名方法开始 |
| 26 | Console.WriteLine("Anonymous Method: {0}", x); | // 主体:打出 Anonymous Method 和 x |
| 27 | }; | // 匿名方法结束;分号是整条赋值语句的结尾 |
| 28 | | |
| 29 | // 使用匿名方法调用委托 | |
| 30 | nc(10); | // 调用委托,x = 10,打出 Anonymous Method: 10 |
大括号后面的分号是整条语句的结尾,不是方法体自己要的。漏写就报错,编译器以为你还要接着声明下一个变量:
| 4 | } | // 匿名方法结束,这里漏了分号 |
Program.cs(4,2): error CS1003: 语法错误,应输入“,”
4匿名方法的两个小例子
delegate 后面的参数要写类型;不用写返回类型,但 return 交出的值要对上 Func 最后那个 int。
| 2 | Func<int, int, int> multiply = delegate (int a, int b) | // multiply 装进匿名方法,参数写明 int a、int b |
| 3 | { | // 匿名方法开始 |
| 4 | return a * b; | // 返回 int,对上 Func 的返回类型 |
| 5 | }; | // 匿名方法结束 |
| 6 | Console.WriteLine(multiply(2, 3)); // 输出 6 | // multiply(2, 3):2 × 3 = 6 |
把 return 改成交出一个字符串,就转换不成这个委托:
| 3 | return "a * b"; | // 交出的是 string,和 Func 要的 int 对不上 |
Program.cs(3,12): error CS0029: 无法将类型“string”隐式转换为“int” Program.cs(3,12): error CS1662: 无法将 匿名方法 转换为预期委托类型,因为块中的某些返回类型不可隐式转换为委托返回类型
第二个例子用匿名方法当按钮的事件处理程序。教程这段直接放进控制台程序会报错,因为 Button 是 Windows 窗体程序里的按钮,控制台程序里没有:
| 9 | Button 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 是附带的事件信息。
| 10 | button.Click += delegate (object sender, EventArgs e) | // 用 += 把匿名方法挂到 Click 事件上 |
| 11 | { | // 匿名方法开始 |
| 12 | Console.WriteLine("Button clicked!"); | // 被点时打出一行字 |
| 13 | }; | // 匿名方法结束 |
| 14 | button.PerformClick(); | // 模拟点一下,挂上的匿名方法被执行 |
查看完整代码(22 行)
| 1 | // 示例:使用匿名方法定义一个委托 | |
| 2 | Func<int, int, int> multiply = delegate (int a, int b) | // multiply 装进匿名方法,两个 int 进,一个 int 出 |
| 3 | { | // 匿名方法开始 |
| 4 | return a * b; | // 返回 a 乘 b |
| 5 | }; | // 匿名方法结束,分号结束整条语句 |
| 6 | Console.WriteLine(multiply(2, 3)); // 输出 6 | // 2 × 3 = 6 |
| 7 | | |
| 8 | // 示例:使用匿名方法作为事件处理程序 | |
| 9 | Button button = new Button(); | // 创建一个按钮对象(用的是下面自己写的 Button) |
| 10 | button.Click += delegate (object sender, EventArgs e) | // 订阅 Click 事件,处理程序是一个匿名方法 |
| 11 | { | // 匿名方法开始 |
| 12 | Console.WriteLine("Button clicked!"); | // 被点时打出 Button clicked! |
| 13 | }; | // 匿名方法结束 |
| 14 | button.PerformClick(); | // 模拟点击,引发 Click 事件 |
| 15 | | |
| 16 | // 控制台程序里没有窗体的 Button,自己写一个最小的 | |
| 17 | delegate void ClickHandler(object sender, EventArgs e); | // 声明事件用的委托类型:接收 sender 和 EventArgs,不返回值 |
| 18 | class 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 先后装了匿名方法和两个有名字的方法,调用写法完全一样。
| 33 | nc = new NumberChanger(AddNum); | // nc 换成有名字的方法 AddNum |
| 34 | | |
| 35 | // 使用命名方法调用委托 | |
| 36 | nc(5); | // 调用 AddNum(5):num = 10 + 5 = 15 |
| 37 | | |
| 38 | // 使用另一个命名方法实例化委托 | |
| 39 | nc = new NumberChanger(MultNum); | // nc 再换成 MultNum |
| 40 | | |
| 41 | // 使用命名方法调用委托 | |
| 42 | nc(2); | // 调用 MultNum(2):num = 15 × 2 = 30 |
查看完整代码(46 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | | |
| 3 | delegate void NumberChanger(int n); | // 声明委托类型:接收一个 int,不返回值 |
| 4 | namespace 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 的值填进大括号的位置。
| 26 | NumberChanger 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</,就是按 C# 2 编译:匿名方法的版本照常运行,输出和上面一样;Lambda 的版本编译报错,另一条是 $ 开头的内插字符串,它到 C# 6 才有:
| 26 | NumberChanger 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 语言那样,用指针直接操作内存?
