C# 委托与多播委托详解
一、什么是委托?
1.1 先从一个生活场景说起
你有没有用过"外卖平台"?你下单后,外卖平台帮你找到骑手去送餐。
委托在 C# 里的角色,就像这个外卖平台:
- 你不需要自己跑到餐厅去取餐
- 你只需要告诉平台"我要这个餐厅的饭"
- 平台帮你找到对应的方法(骑手)去执行
委托就是用来"传递方法"的工具——把方法当作参数传给另一个方法。
1.2 为什么需要委托?
没有委托时,你有 3 个不同的操作:
// 三个不同的操作
void Add(int a, int b) { Console.WriteLine(a + b); }
void Multiply(int a, int b) { Console.WriteLine(a * b); }
void Subtract(int a, int b) { Console.WriteLine(a - b); }
// ❌ 想统一调用它们,只能写死
Add(3, 5);
Multiply(3, 5);
Subtract(3, 5);
有个需求:用户选什么操作,就执行什么操作。传统的写法是这样:
string operation = "Add"; // 用户选择的
// ❌ 只能用 if-else 或者 switch,每加一个新操作都要改代码
if (operation == "Add") Add(3, 5);
else if (operation == "Mul") Multiply(3, 5);
else if (operation == "Sub") Subtract(3, 5);
有了委托后,你可以把方法当成参数传进去:
// ✅ 定义一个委托类型——它说的是"我的方法长什么样"
delegate void Calculate(int a, int b);
// 把方法当成参数
static void Execute(Calculate calc, int x, int y)
{
calc(x, y); // 执行传进来的方法
}
// 调用时,直接传方法名
Execute(Add, 3, 5); // 输出: 8
Execute(Multiply, 3, 5); // 输出: 15
Execute(Subtract, 3, 5); // 输出: -2
一句话总结:委托 = 方法的"传送带",让你把方法像数据一样传来传去。
二、定义和使用委托
2.1 定义委托(四步走)
// 语法:delegate 返回值类型 委托名称(参数列表);
// 步骤1:定义一个委托类型(说清楚这个委托能装什么形状的方法)
delegate void MyDelegate(string message);
// 步骤2:写几个匹配的方法("形状"要一模一样:返回值 + 参数列表)
static void PrintEnglish(string msg) { Console.WriteLine($"英文: {msg}"); }
static void PrintChinese(string msg) { Console.WriteLine($"中文: {msg}"); }
static void PrintUppercase(string msg) { Console.WriteLine($"大写: {msg.ToUpper()}"); }
// 步骤3:创建委托实例,绑定方法
MyDelegate del1 = PrintEnglish; // 新建语法
MyDelegate del2 = new MyDelegate(PrintChinese); // 老式语法,也能用
// 步骤4:调用委托(就像直接调用方法一样)
del1("Hello"); // 输出: 英文: Hello
del2("你好"); // 输出: 中文: 你好
2.2 完整基础示例
using System;
// 1. 定义委托
delegate int MathOperation(int a, int b);
class Program
{
// 2. 写几个匹配的方法
static int Add(int a, int b) => a + b;
static int Subtract(int a, int b) => a - b;
static int Multiply(int a, int b) => a * b;
static int Divide(int a, int b) => a / b;
// 3. 用委托作为参数——"计算器核心"
static int Calculator(MathOperation op, int x, int y)
{
return op(x, y); // 调用传进来的方法
}
static void Main()
{
int result;
result = Calculator(Add, 10, 3); // 输出: 13
Console.WriteLine($"10 + 3 = {result}");
result = Calculator(Subtract, 10, 3); // 输出: 7
Console.WriteLine($"10 - 3 = {result}");
result = Calculator(Multiply, 10, 3); // 输出: 30
Console.WriteLine($"10 * 3 = {result}");
result = Calculator(Divide, 10, 3); // 输出: 3
Console.WriteLine($"10 / 3 = {result}");
}
}
输出:
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3
三、委托的本质理解(图文详解)
3.1 委托 = 方法的"容器"
委托就像一个方法签名模板,只有"形状匹配"的方法才能放进去。
委托类型:delegate int MathOperation(int a, int b);
└──返回值──┘ └──参数──┘
能装的方法:
✅ int Add(int a, int b) → 形状匹配
✅ int Subtract(int a, int b) → 形状匹配
✅ int Multiply(int a, int b) → 形状匹配
❌ void Print(string s) → 形状不匹配(返回值是 void)
❌ int Square(int x) → 只能算是匹配,但语义上没问题
❌ double Divide(int a, int b)→ 形状不匹配(返回值是 double)
3.2 委托的内部原理(简化理解)
MathOperation op = Add;
内存中的 op:
┌──────────────────────┐
│ op:委托实例 │
│ ┌────────────────┐ │
│ │ Target: null │ │ ← 静态方法时是 null
│ │ Method: Add │ │ ← 指向 Add 方法的地址
│ └────────────────┘ │
└──────────────────────┘
当你调用 op(10, 3) 时,等价于调用了 Add(10, 3)
四、委托的三种创建方式
delegate void MyDelegate(string msg);
static void Print(string msg) => Console.WriteLine(msg);
// 方式1:直接赋值(最常用)
MyDelegate d1 = Print;
// 方式2:完整 new 语法(老式写法)
MyDelegate d2 = new MyDelegate(Print);
// 方式3:匿名方法(C# 2.0)
MyDelegate d3 = delegate(string msg)
{
Console.WriteLine($"匿名方法说: {msg}");
};
// 方式4:Lambda 表达式(C# 3.0+,最简洁)
MyDelegate d4 = msg => Console.WriteLine($"Lambda 说: {msg}");
// 方式5:已有方法的 Lambda
MyDelegate d5 = msg => Print(msg.ToUpper());
// 全部都能调
d1("Hello"); // Hello
d2("Hello"); // Hello
d3("Hello"); // 匿名方法说: Hello
d4("Hello"); // Lambda 说: Hello
d5("Hello"); // HELLO
五、多播委托
5.1 什么是多播委托?
多播委托 = 一个委托"链"上可以挂多个方法,调用一次,所有方法依次执行。
打个比喻:
普通委托像给一个人打电话——你说一句话,一个人听到。
多播委托像广播喇叭——你说一句话,全校人都能听到。
5.2 创建多播委托(用 + 号串联)
delegate void Notify(string msg);
static void SendEmail(string msg) => Console.WriteLine($"📧 邮件通知: {msg}");
static void SendSMS(string msg) => Console.WriteLine($"📱 短信通知: {msg}");
static void SendAppPush(string msg) => Console.WriteLine($"🔔 APP推送: {msg}");
static void LogToFile(string msg) => Console.WriteLine($"📝 日志记录: {msg}");
static void Main()
{
// 一步步串联
Notify notifier = SendEmail; // 先挂上邮件
notifier += SendSMS; // 再挂上短信
notifier += SendAppPush; // 再挂上 APP 推送
notifier += LogToFile; // 再挂上日志
// 调用一次,四个方法全部执行!
Console.WriteLine("=== 多播委托调用 ===");
notifier("您的订单已发货!");
}
输出:
=== 多播委托调用 ===
📧 邮件通知: 您的订单已发货!
📱 短信通知: 您的订单已发货!
🔔 APP推送: 您的订单已发货!
📝 日志记录: 您的订单已发货!
5.3 移除方法(用 - 号)
Notify notifier = SendEmail;
notifier += SendSMS;
notifier += SendAppPush;
notifier += LogToFile;
Console.WriteLine($"当前挂载了 {notifier.GetInvocationList().Length} 个方法");
// 移除短信通知
notifier -= SendSMS;
Console.WriteLine($"移除短信后,剩 {notifier.GetInvocationList().Length} 个方法");
Console.WriteLine("\n=== 移除后调用 ===");
notifier("系统维护中...");
输出:
当前挂载了 4 个方法
移除短信后,剩 3 个方法
=== 移除后调用 ===
📧 邮件通知: 系统维护中...
🔔 APP推送: 系统维护中...
📝 日志记录: 系统维护中...
5.4 多播委托的返回值(重要!)
如果委托有返回值,多播委托只返回最后一个方法的返回值:
delegate int GetNumber();
static int GetOne() { Console.WriteLine("GetOne 执行"); return 1; }
static int GetTwo() { Console.WriteLine("GetTwo 执行"); return 2; }
static int GetThree() { Console.WriteLine("GetThree 执行"); return 3; }
static void Main()
{
GetNumber chain = GetOne;
chain += GetTwo;
chain += GetThree;
int result = chain(); // 三个方法都执行了!
Console.WriteLine($"\n返回的值: {result}"); // 3 ← 只有最后一个的返回值!
}
输出:
GetOne 执行
GetTwo 执行
GetThree 执行
返回的值: 3
注意:中间方法的返回值全部被丢弃!如果每个返回值都重要,需要手动遍历。
5.5 手动获取每个方法的返回值
GetNumber chain = GetOne;
chain += GetTwo;
chain += GetThree;
// 手动遍历委托链中的每个方法
Delegate[] methods = chain.GetInvocationList();
foreach (GetNumber method in methods)
{
int result = method();
Console.WriteLine($" → 返回: {result}");
}
输出:
GetOne 执行
→ 返回: 1
GetTwo 执行
→ 返回: 2
GetThree 执行
→ 返回: 3
5.6 多播委托的异常处理
如果一个方法抛异常,后面的方法不会执行:
delegate void Action();
static void Do1() { Console.WriteLine("步骤1 完成"); }
static void Do2() { Console.WriteLine("步骤2 完成"); }
static void Do3() { throw new Exception("步骤3 出错!"); }
static void Do4() { Console.WriteLine("步骤4 完成"); }
Action chain = Do1;
chain += Do2;
chain += Do3;
chain += Do4;
try
{
chain();
}
catch (Exception ex)
{
Console.WriteLine($"捕获异常: {ex.Message}");
// Do4 没有执行!
}
输出:
步骤1 完成
步骤2 完成
捕获异常: 步骤3 出错!
安全的做法——手动遍历:
Action chain = Do1;
chain += Do2;
chain += Do3;
chain += Do4;
foreach (Action method in chain.GetInvocationList())
{
try
{
method();
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ 方法执行失败: {ex.Message}");
}
}
输出:
步骤1 完成
步骤2 完成
⚠️ 方法执行失败: 步骤3 出错!
步骤4 完成 ← 继续执行了!
六、Action、Func、Predicate —— 内置委托(不用自己定义!)
C# 已经帮你定义好了常用的委托类型,绝大多数情况下不需要自己定义。
6.1 Action —— 无返回值
// Action:可以有 0~16 个参数,无返回值
Action hello = () => Console.WriteLine("你好!");
Action<string> print = msg => Console.WriteLine(msg);
Action<string, int> repeat = (msg, n) =>
{
for (int i = 0; i < n; i++)
Console.Write(msg);
Console.WriteLine();
};
hello(); // 你好!
print("C# 真好学"); // C# 真好学
repeat("⭐", 5); // ⭐⭐⭐⭐⭐
6.2 Func —— 有返回值
// Func:最后一个类型参数是返回值类型,前面是参数类型
// Func<TResult> —— 无参数,返回 TResult
Func<int> getRandom = () => new Random().Next(1, 100);
Console.WriteLine(getRandom()); // 比如: 42
// Func<T, TResult> —— 一个参数,返回 TResult
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 25
// Func<T1, T2, TResult> —— 两个参数,返回 TResult
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 7)); // 10
// Func<T1, T2, T3, TResult> —— 三个参数
Func<string, string, string> concat = (s1, s2) => s1 + s2;
Console.WriteLine(concat("Hello", "World")); // HelloWorld
6.3 Predicate —— 返回 bool
// Predicate<T>:接收一个 T 参数,返回 bool(等价于 Func<T, bool>)
Predicate<int> isPositive = n => n > 0;
Console.WriteLine(isPositive(5)); // True
Console.WriteLine(isPositive(-3)); // False
// 常用于集合的 Find、FindAll 等方法
List<int> nums = new List<int> { -3, -1, 0, 2, 5, 8 };
int firstPositive = nums.Find(isPositive);
Console.WriteLine(firstPositive); // 2
6.4 内置委托速查表
| 委托类型 | 参数 | 返回值 | 示例用法 |
|---|---|---|---|
Action |
无 | 无 | Action act = () => ... |
Action<T> |
1 个 | 无 | Action<string> print = s => ... |
Action<T1,T2> |
2 个 | 无 | Action<int,int> = (a,b) => ... |
| ... | 最多 16 | 无 | |
Func<TResult> |
无 | 1 个 | Func<int> r = () => 42 |
Func<T,TResult> |
1 个 | 1 个 | Func<int,int> sq = x => x*x |
Func<T1,T2,TResult> |
2 个 | 1 个 | Func<int,int,int> add |
| ... | 最多 16 | 1 个 | |
Predicate<T> |
1 个 | bool |
Predicate<int> p = x => x>0 |
建议:尽量不要自己定义委托了,直接用
Action、Func就行。
七、重写上面的例子——用 Func 代替自定义委托
把之前的计算器改成用 Func:
using System;
class Program
{
static int Add(int a, int b) => a + b;
static int Subtract(int a, int b) => a - b;
static int Multiply(int a, int b) => a * b;
static int Divide(int a, int b) => a / b;
// ✅ 参数不用自定义 delegate,直接写 Func<int, int, int>
static int Calculator(Func<int, int, int> operation, int x, int y)
{
return operation(x, y);
}
static void Main()
{
Console.WriteLine($"10 + 3 = {Calculator(Add, 10, 3)}");
Console.WriteLine($"10 - 3 = {Calculator(Subtract, 10, 3)}");
Console.WriteLine($"10 * 3 = {Calculator(Multiply, 10, 3)}");
Console.WriteLine($"10 / 3 = {Calculator(Divide, 10, 3)}");
// 甚至可以直接传 Lambda!
Console.WriteLine($"取最大值: {Calculator((a, b) => a > b ? a : b, 10, 3)}");
}
}
八、委托的实际应用场景
8.1 场景一:回调函数(异步操作完成后通知)
// 模拟下载文件,完成后回调
static void DownloadFile(string url, Action<string> onComplete)
{
Console.WriteLine($"正在从 {url} 下载...");
System.Threading.Thread.Sleep(1000); // 模拟耗时
string result = "文件内容xxx";
onComplete(result); // 下载完通知
}
// 使用
DownloadFile("https://example.com/file.txt", content =>
{
Console.WriteLine($"下载完成!内容: {content}");
});
8.2 场景二:事件处理(事件的基础就是委托)
// 按钮点击就是典型的委托应用
button.Click += (sender, e) => Console.WriteLine("按钮被点了!");
8.3 场景三:过滤/筛选条件
// 一个通用的过滤方法
static List<T> Filter<T>(List<T> list, Predicate<T> condition)
{
List<T> result = new List<T>();
foreach (T item in list)
{
if (condition(item))
result.Add(item);
}
return result;
}
// 使用
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = Filter(numbers, n => n % 2 == 0);
var bigNumbers = Filter(numbers, n => n > 5);
Console.WriteLine($"偶数: {string.Join(", ", evenNumbers)}"); // 2, 4, 6, 8, 10
Console.WriteLine($"大于5: {string.Join(", ", bigNumbers)}"); // 6, 7, 8, 9, 10
8.4 场景四:策略模式(动态切换算法)
// 不同的排序策略
Func<int[], int[]> bubbleSort = arr =>
{
Console.WriteLine("使用冒泡排序");
// ... 实现略
return arr;
};
Func<int[], int[]> quickSort = arr =>
{
Console.WriteLine("使用快速排序");
// ... 实现略
return arr;
};
// 根据数组大小选择排序策略
int[] data = new int[1000];
Func<int[], int[]> chosenSort = data.Length < 100 ? bubbleSort : quickSort;
chosenSort(data);
8.5 完整实战示例:简易任务调度系统
using System;
using System.Collections.Generic;
class Program
{
// 任务队列:存要执行的方法
static List<Action> tasks = new List<Action>();
static void AddTask(Action task)
{
tasks.Add(task);
Console.WriteLine($"任务已添加,当前共 {tasks.Count} 个任务");
}
static void RunAllTasks()
{
Console.WriteLine($"\n===== 开始执行 {tasks.Count} 个任务 =====");
for (int i = 0; i < tasks.Count; i++)
{
Console.Write($"任务 {i + 1}: ");
tasks[i]();
}
tasks.Clear();
Console.WriteLine("===== 全部完成 =====\n");
}
static void Main()
{
// 添加各种任务
AddTask(() => Console.WriteLine("备份数据库...完成 ✅"));
AddTask(() => Console.WriteLine("发送日报邮件...完成 ✅"));
AddTask(() => Console.WriteLine("清理临时文件...完成 ✅"));
AddTask(() =>
{
// 复杂任务
Console.Write("生成月度报表...");
System.Threading.Thread.Sleep(500); // 模拟耗时
Console.WriteLine("完成 ✅");
});
// 一键执行
RunAllTasks();
// 再添加一些任务
AddTask(() => Console.WriteLine("系统健康检查...完成 ✅"));
AddTask(() => Console.WriteLine("更新缓存...完成 ✅"));
RunAllTasks();
}
}
输出:
任务已添加,当前共 1 个任务
任务已添加,当前共 2 个任务
任务已添加,当前共 3 个任务
任务已添加,当前共 4 个任务
===== 开始执行 4 个任务 =====
任务 1: 备份数据库...完成 ✅
任务 2: 发送日报邮件...完成 ✅
任务 3: 清理临时文件...完成 ✅
任务 4: 生成月度报表...完成 ✅
===== 全部完成 =====
任务已添加,当前共 1 个任务
任务已添加,当前共 2 个任务
===== 开始执行 2 个任务 =====
任务 1: 系统健康检查...完成 ✅
任务 2: 更新缓存...完成 ✅
===== 全部完成 =====
九、匿名方法与 Lambda 表达式
委托的发展历程——越来越简洁:
// 1.0 时代:命名方法
delegate int Operation(int x);
static int DoubleIt(int x) { return x * 2; }
Operation op1 = DoubleIt;
// 2.0 时代:匿名方法
Operation op2 = delegate(int x) { return x * 2; };
// 3.0 时代:Lambda 表达式
Operation op3 = (int x) => { return x * 2; };
// 再简化:省略类型
Operation op4 = (x) => { return x * 2; };
// 最简:单表达式可以去掉花括号和 return
Operation op5 = x => x * 2;
// 全部等价,结果都是 20
Console.WriteLine(op1(10)); // 20
Console.WriteLine(op2(10)); // 20
Console.WriteLine(op3(10)); // 20
Console.WriteLine(op4(10)); // 20
Console.WriteLine(op5(10)); // 20
Lambda 表达式语法速记:
// 无参数
Func<int> f1 = () => 42;
// 一个参数(括号可省略)
Func<int, int> f2 = x => x * x;
// 多个参数
Func<int, int, int> f3 = (x, y) => x + y;
// 多行语句(需要花括号 + return)
Func<int, int> f4 = x =>
{
int result = x * 2;
Console.WriteLine($"计算中: {x} * 2 = {result}");
return result;
};
// 无参数无返回值
Action act = () => Console.WriteLine("执行完毕");
十、协变与逆变(进阶,了解即可)
// 协变(返回值):委托可以返回比定义更"具体"的类型
class Animal { }
class Dog : Animal { }
delegate Animal AnimalFactory(); // 返回 Animal
static Dog CreateDog() => new Dog();
AnimalFactory factory = CreateDog; // ✅ 返回 Dog 的也可以
Animal result = factory(); // 运行时返回的是 Dog
// 逆变(参数):委托可以接收比定义更"宽泛"的类型
delegate void DogHandler(Dog d); // 接收 Dog 参数
static void HandleAnimal(Animal a) => Console.WriteLine("处理动物");
DogHandler handler = HandleAnimal; // ✅ 接收 Animal 的也可以
handler(new Dog()); // 传入 Dog,实际当作 Animal 处理
这部分初学者可以先跳过,理解委托基础后再回来看。
十一、常见易错点(避坑指南)
坑1:多播委托有返回值只取最后一个
Func<int> chain = () => 1;
chain += () => 2;
chain += () => 3;
Console.WriteLine(chain()); // 3 ← 不是 6!
坑2:多播委托中一个方法报错,后面都不执行
Action chain = () => Console.WriteLine("A");
chain += () => throw new Exception("炸了!");
chain += () => Console.WriteLine("B"); // ❌ 永远不会执行!
坑3:委托是 null 时调用会报错
Action act = null;
// act(); // ❌ NullReferenceException!
// ✅ 安全调用
act?.Invoke(); // C# 6.0+ 空条件运算符
// 或
if (act != null) act();
坑4:Lambda 中捕获的变量是引用
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var act in actions)
act();
// 输出: 3, 3, 3 ← 全是 3!因为 i 是同一个变量
正确做法——用局部变量捕获:
for (int i = 0; i < 3; i++)
{
int copy = i; // 每次循环都创建一个新变量
actions[i] = () => Console.WriteLine(copy);
}
foreach (var act in actions)
act();
// 输出: 0, 1, 2 ✅
坑5:-= 移除方法时注意事项
Action act = () => Console.WriteLine("A");
act += () => Console.WriteLine("B");
// ✅ 同一个 Lambda 不能用来移除,因为每次写 Lambda 都是新对象
act += () => Console.WriteLine("C");
act -= () => Console.WriteLine("C"); // ❌ 删不掉的!
// 这两个 () => Console.WriteLine("C") 是不同的对象
十二、总结
委托核心概念速查
| 概念 | 说明 |
|---|---|
| 委托是什么 | 方法的"容器"或"传送带",可以把方法当参数传递 |
| 定义 | delegate 返回值 名称(参数列表); |
| 内置委托 | Action(无返回)、Func(有返回)、Predicate(返回 bool) |
| 多播委托 | 用 + 串联多个方法,调用一次全部执行 |
| 移除方法 | 用 - 从链上移除 |
| Lambda | x => x * 2,创建委托的最简写法 |
委托 vs 多播委托
| 特点 | 普通委托 | 多播委托 |
|---|---|---|
| 挂载方式 | del = method |
del += method1; del += method2 |
| 方法个数 | 1 个 | 多个 |
| 调用方式 | del() |
del()(一模一样) |
| 返回值 | 正常返回 | 只返回最后一个的 |
| 异常 | 方法报错→调用者捕获 | 一个报错→后面全部不执行 |
| 移除 | del = null |
del -= method |
记忆口诀
委托就是把方法当快递寄,
Action 不带回执 Func 有回执,
加号串联多播全执行,
Lambda 一行搞定最省事。
一句话总结:委托是 C# 中把方法当参数传递的核心机制,用
Action/Func不用自己定义,用+号可以串联成多播委托让多个方法依次执行。它是一切事件、回调、LINQ 的基础。
评论区