1委托是什么
委托是一种引用类型,委托对象里存的是对某个方法的引用。它像 C/C++ 的函数指针,但会检查方法的“样子”对不对得上。事件、回调、LINQ 背后都是委托。
| 9 | Console.WriteLine(typeof(NumberChanger).BaseType); | // typeof 取到委托类型,.BaseType 是它的父类 |
| 10 | Console.WriteLine(typeof(MulticastDelegate).BaseType); | // 再往上一层的父类 |
System.MulticastDelegate System.Delegate
教程说委托都派生自 System.Delegate;实测是先继承 MulticastDelegate,再往上一层才是 System.Delegate。
2声明委托
| 3 | public delegate int MathOperation(int x, int y); | // 能装“接收两个 int、返回 int”的方法 |
| 4 | public delegate int MyDelegate(string s); | // 能装“接收一个 string、返回 int”的方法 |
delegate 后面依次是返回类型、委托名、参数列表。参数和返回值合起来叫签名(教程说的“相同标签”就是签名),签名对得上的方法才能装进去。视频里把它比成插卡导轨:插座的形状是委托类型,插上卡是委托对象,按按钮就是调用。
3创建委托并调用
例子里 num 初值 10,AddNum 给它加参数,MultNum 给它乘参数,getNum 读出它。
| 28 | NumberChanger nc1 = new NumberChanger(AddNum); | // 创建委托 nc1,装着 AddNum(方法名不带括号) |
| 29 | NumberChanger nc2 = new NumberChanger(MultNum); | // 创建委托 nc2,装着 MultNum |
| 30 | // 使用委托对象调用方法 | |
| 31 | nc1(25); | // 调用委托 = 调用 AddNum(25):num = 10 + 25 = 35 |
| 32 | Console.WriteLine("Value of Num: {0}", getNum()); | // 输出 35 |
| 33 | nc2(5); | // 调用 MultNum(5):num 已是 35,35 × 5 = 175 |
| 34 | Console.WriteLine("Value of Num: {0}", getNum()); | // 输出 175 |
查看完整代码(38 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | | |
| 3 | delegate int NumberChanger(int n); | // 声明委托类型:接收 int,返回 int |
| 4 | namespace DelegateAppl | // 定义命名空间 DelegateAppl |
| 5 | { | // 命名空间开始 |
| 6 | class TestDelegate | // 定义 TestDelegate 类 |
| 7 | { | // 类开始 |
| 8 | static int num = 10; | // 静态变量 num,初值 10 |
| 9 | public static int AddNum(int p) | // AddNum:签名是“接收 int,返回 int”,和委托对得上 |
| 10 | { | // AddNum 开始 |
| 11 | num += p; | // num 加上参数 p |
| 12 | return num; | // 返回新的 num |
| 13 | } | // AddNum 结束 |
| 14 | | |
| 15 | public static int MultNum(int q) | // MultNum:签名同样对得上 |
| 16 | { | // MultNum 开始 |
| 17 | num *= q; | // num 乘上参数 q |
| 18 | return num; | // 返回新的 num |
| 19 | } | // MultNum 结束 |
| 20 | public static int getNum() | // getNum:不接收参数,只返回 num |
| 21 | { | // getNum 开始 |
| 22 | return num; | // 返回当前的 num |
| 23 | } | // getNum 结束 |
| 24 | | |
| 25 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 26 | { | // Main 开始 |
| 27 | // 创建委托实例 | |
| 28 | NumberChanger nc1 = new NumberChanger(AddNum); | // 创建委托 nc1,里面装着 AddNum |
| 29 | NumberChanger nc2 = new NumberChanger(MultNum); | // 创建委托 nc2,里面装着 MultNum |
| 30 | // 使用委托对象调用方法 | |
| 31 | nc1(25); | // 调用 nc1,等于调用 AddNum(25):num = 10 + 25 = 35 |
| 32 | Console.WriteLine("Value of Num: {0}", getNum()); | // 输出 num:35 |
| 33 | nc2(5); | // 调用 nc2,等于调用 MultNum(5):num = 35 × 5 = 175 |
| 34 | Console.WriteLine("Value of Num: {0}", getNum()); | // 输出 num:175 |
| 35 | Console.ReadKey(); | // 等待按键,防止控制台窗口一闪而过 |
| 36 | } | // Main 结束 |
| 37 | } | // 类结束 |
| 38 | } | // 命名空间结束 |
Value of Num: 35 Value of Num: 175
从 C# 2.0 起可以不写 new,直接写方法名,输出不变:
| 28 | NumberChanger nc1 = AddNum; | // 编译器自动创建委托 |
| 29 | NumberChanger nc2 = MultNum; | // 同样直接写方法名,nc2 装着 MultNum |
签名对不上就装不进去,这叫类型安全:
| 28 | NumberChanger nc1 = new NumberChanger(getNum); | // getNum 不接收参数,和委托签名对不上 |
Program.cs(28,30): error CS0123: “getNum”没有与委托“NumberChanger”匹配的重载
4多播
一个委托可以装好几个方法:+= 加进调用列表,-= 拿掉,调用时按顺序执行,只有同一种委托类型才能合并。
| 31 | nc = nc1; | // 调用列表:[AddNum] |
| 32 | nc += nc2; | // 调用列表:[AddNum, MultNum] |
| 33 | // 调用多播 | |
| 34 | nc(5); | // 先 AddNum:10 + 5 = 15,再 MultNum:15 × 5 = 75 |
Value of Num: 75
合并委托返回的是最后一个方法的返回值。+= 会生成一个新委托赋给左边,nc1、nc2 本身不变。
5委托当参数:回调
| 29 | public static void sendString(printString ps) | // 参数 ps 是一个委托 |
| 30 | { | // sendString 开始 |
| 31 | ps("Hello World"); | // 只管调用,具体做什么看传进来的方法 |
| 32 | } | // sendString 结束 |
| 33 | static void Main(string[] args) | // 程序入口 |
| 34 | { | // Main 开始 |
| 35 | printString ps1 = new printString(WriteToScreen); | // ps1 装着“打到屏幕” |
| 36 | printString ps2 = new printString(WriteToFile); | // ps2 装着“写进文件” |
| 37 | sendString(ps1); | // 控制台输出一行 |
| 38 | sendString(ps2); | // 写进 message.txt,控制台看不到 |
查看完整代码(42 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | using System.IO; | // 引入 System.IO,文件读写用的类在这里 |
| 3 | | |
| 4 | namespace DelegateAppl | // 定义命名空间 |
| 5 | { | // 命名空间开始 |
| 6 | class PrintString | // 定义 PrintString 类 |
| 7 | { | // 类开始 |
| 8 | static FileStream fs; | // 文件流,写文件时用 |
| 9 | static StreamWriter sw; | // 写入器,往文件流里写文字 |
| 10 | // 委托声明 | |
| 11 | public delegate void printString(string s); | // 委托 printString:接收 string,不返回值 |
| 12 | | |
| 13 | // 该方法打印到控制台 | |
| 14 | public static void WriteToScreen(string str) | // WriteToScreen:签名和委托对得上 |
| 15 | { | // WriteToScreen 开始 |
| 16 | Console.WriteLine("The String is: {0}", str); | // 把字符串打到控制台 |
| 17 | } | // WriteToScreen 结束 |
| 18 | // 该方法打印到文件 | |
| 19 | public static void WriteToFile(string s) | // WriteToFile:签名也对得上 |
| 20 | { | // WriteToFile 开始 |
| 21 | fs = new FileStream("c:\\message.txt", FileMode.Append, FileAccess.Write); | // 以追加方式打开文件 c:\message.txt(字符串里 \\ 代表一个 \) |
| 22 | sw = new StreamWriter(fs); | // 在文件流上创建写入器 |
| 23 | sw.WriteLine(s); | // 写入一行字符串 |
| 24 | sw.Flush(); | // 把缓冲区里的内容真正写进文件 |
| 25 | sw.Close(); | // 关闭写入器 |
| 26 | fs.Close(); | // 关闭文件流 |
| 27 | } | // WriteToFile 结束 |
| 28 | // 该方法把委托作为参数,并使用它调用方法 | |
| 29 | public static void sendString(printString ps) | // sendString 的参数 ps 是一个委托 |
| 30 | { | // sendString 开始 |
| 31 | ps("Hello World"); | // 用 "Hello World" 调用委托,具体做什么看传进来的方法 |
| 32 | } | // sendString 结束 |
| 33 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 34 | { | // Main 开始 |
| 35 | printString ps1 = new printString(WriteToScreen); | // ps1 装着 WriteToScreen |
| 36 | printString ps2 = new printString(WriteToFile); | // ps2 装着 WriteToFile |
| 37 | sendString(ps1); | // 传 ps1:在控制台打出一行 |
| 38 | sendString(ps2); | // 传 ps2:写进文件,控制台上看不到 |
| 39 | Console.ReadKey(); | // 等待按键 |
| 40 | } | // Main 结束 |
| 41 | } | // 类结束 |
| 42 | } | // 命名空间结束 |
The String is: Hello World
控制台只有一行,第二次的 Hello World 写进了文件。教程路径是 C 盘根目录,我们在 macOS 上实测,文件写在了运行目录里。
6用 -= 移除
| 17 | PrintMessage print = PrintUpperCase; | // 调用列表:[大写] |
| 18 | print += PrintLowerCase; | // 调用列表:[大写, 小写] |
| 19 | | |
| 20 | // 移除 PrintLowerCase 方法 | |
| 21 | print -= PrintLowerCase; | // 拿掉小写,回到 [大写] |
| 22 | | |
| 23 | // 调用委托(只会调用 PrintUpperCase) | |
| 24 | print("Hello, C#"); // 输出: HELLO, C# | // 只执行 PrintUpperCase |
查看完整代码(26 行)
| 1 | public class Program | // 定义 Program 类 |
| 2 | { | // 类开始 |
| 3 | public delegate void PrintMessage(string message); | // 委托 PrintMessage:接收 string,不返回值 |
| 4 | | |
| 5 | public static void PrintUpperCase(string message) | // 方法:输出大写 |
| 6 | { | // PrintUpperCase 开始 |
| 7 | Console.WriteLine(message.ToUpper()); | // 把字符串转成大写后输出 |
| 8 | } | // PrintUpperCase 结束 |
| 9 | | |
| 10 | public static void PrintLowerCase(string message) | // 方法:输出小写 |
| 11 | { | // PrintLowerCase 开始 |
| 12 | Console.WriteLine(message.ToLower()); | // 把字符串转成小写后输出 |
| 13 | } | // PrintLowerCase 结束 |
| 14 | | |
| 15 | public static void Main() | // 程序入口 Main 方法 |
| 16 | { | // Main 开始 |
| 17 | PrintMessage print = PrintUpperCase; | // print 先装 PrintUpperCase:调用列表 [大写] |
| 18 | print += PrintLowerCase; | // 再加上 PrintLowerCase:调用列表 [大写, 小写] |
| 19 | | |
| 20 | // 移除 PrintLowerCase 方法 | |
| 21 | print -= PrintLowerCase; | // 拿掉 PrintLowerCase:调用列表回到 [大写] |
| 22 | | |
| 23 | // 调用委托(只会调用 PrintUpperCase) | |
| 24 | print("Hello, C#"); // 输出: HELLO, C# | // 只执行 PrintUpperCase |
| 25 | } | // Main 结束 |
| 26 | } | // 类结束 |
HELLO, C#
不写 -= 那一行,会输出 HELLO, C# 和 hello, c# 两行。VS 会在这两行画绿色波浪线(警告 CS8601、CS8602):减完后委托可能变成 null,调用前最好先判断,写法见下面的 if (。
7委托和事件
| 4 | public event EventHandler Click; | // 用 event 声明事件,EventHandler 是一种委托 |
| 23 | button.Click += Button_Click; | // 订阅:把 Button_Click 挂到 Click 上 |
查看完整代码(33 行)
| 1 | public class Button | // 定义 Button 类(发布者) |
| 2 | { | // 类开始 |
| 3 | // 定义一个事件 | |
| 4 | public event EventHandler Click; | // 用 event 声明事件 Click,类型 EventHandler 是一种委托 |
| 5 | | |
| 6 | // 引发事件的方法 | |
| 7 | public void OnClick() | // OnClick:引发事件 |
| 8 | { | // OnClick 开始 |
| 9 | if (Click != null) | // 先确认有人订阅(不是空) |
| 10 | { | // if 开始 |
| 11 | Click(this, EventArgs.Empty); // 调用事件 | // 调用事件:挂在上面的方法被依次执行 |
| 12 | } | // if 结束 |
| 13 | } | // OnClick 结束 |
| 14 | } | // 类结束 |
| 15 | | |
| 16 | public class Program | // 定义 Program 类(订阅者) |
| 17 | { | // 类开始 |
| 18 | public static void Main() | // 程序入口 Main 方法 |
| 19 | { | // Main 开始 |
| 20 | Button button = new Button(); | // 创建一个按钮对象 |
| 21 | | |
| 22 | // 订阅事件 | |
| 23 | button.Click += Button_Click; | // 用 += 把 Button_Click 挂到 Click 上 |
| 24 | | |
| 25 | // 引发事件 | |
| 26 | button.OnClick(); // 输出 "Button clicked!" | // 引发事件,Button_Click 被调用 |
| 27 | } | // Main 结束 |
| 28 | | |
| 29 | private static void Button_Click(object sender, EventArgs e) | // 事件处理方法,签名和 EventHandler 对得上 |
| 30 | { | // Button_Click 开始 |
| 31 | Console.WriteLine("Button clicked!"); | // 输出一行字 |
| 32 | } | // Button_Click 结束 |
| 33 | } | // 类结束 |
Button clicked!
事件是包着委托的成员,就像属性包着字段。在 Button 类外面只能 +=、-=,直接调用会报错:
| 26 | button.Click(null, EventArgs.Empty); | // 在类外面直接调用事件 |
Program.cs(26,16): error CS0070: 事件“Button.Click”只能出现在 += 或 -= 的左边(从类型“Button”中使用时除外)
8现成的委托
.NET 准备好了三种委托,不用自己声明(下面是顶级语句写法,不写类和 Main):
| 1 | Action<string> printMessage = Console.WriteLine; | // Action:不返回值;这里接收一个 string |
| 2 | printMessage("Hello"); | // 输出 Hello |
| 1 | Func<int, int, int> add = (x, y) => x + y; | // Func:最后一个类型是返回值,前面都是输入;=> 是 lambda 写法 |
| 2 | Console.WriteLine(add(3, 4)); // 输出 7 | // 3 + 4,输出 7 |
| 1 | Predicate<int> isEven = x => x % 2 == 0; | // Predicate:接收一个参数,固定返回 bool |
| 2 | Console.WriteLine(isEven(4)); // 输出 True | // 4 是偶数,输出 True |
9注意事项
- 匿名方法和 lambda:可以当场写方法交给委托,两种写法结果都是 8。
| 1 | Func<int, int, int> add = delegate (int x, int y) { return x + y; }; | // 匿名方法:delegate 关键字 + 参数 + 方法体 |
| 2 | Console.WriteLine(add(5, 3)); // 输出 8 | // 5 + 3,输出 8 |
- 异步调用:教程里的
BeginInvoke只能在老的 .NET Framework 上用,现在会抛异常,异步改用async和await。
| 4 | add.BeginInvoke(5, 3, null, null); | // 老写法的异步调用 |
PlatformNotSupportedException Operation is not supported on this platform.
10小结
- 委托是一种类型,签名决定它能装哪些方法。
- 用
new或直接写方法名创建;调用委托就是调用里面的方法。 +=、-=增删调用列表,按顺序执行,返回最后一个的值。- 委托能当参数传,做回调;Action、Func、Predicate 是现成的委托。
下一节:包上 event 以后,为什么外面只能订阅、不能直接调用?
