委托基础与多播
本教程共 100 篇 · 第 54 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
54. 委托基础与多播
本节目标:学完你能定义委托类型、把方法塞进委托,并用多播一次调用多个方法。
现实里常有这种需求:排序时比较规则由调用方定,但排序算法由你写。换句话说,算法的某一步要「晚一点绑定」到调用方提供的方法。C# 用委托(delegate) 来干这件事——它把「方法」当成可传递的值。
把委托想象成「遥控器上的自定义按键」:你事先把一个操作(方法)录进去,之后按一下就执行那个操作,而且录进去的可以是不同厂家的设备(不同方法)。同一颗按键,今天控灯、明天控空调,灵活就来自「按键」和「具体操作」解耦。
一、委托是类型安全的函数包装
用 delegate 关键字先声明一种委托类型,描述「长什么样的方法」能装进来:
namespace CSharpDemo;
delegate int MathOp(int a, int b);
int Add(int x, int y) => x + y;
int Mul(int x, int y) => x * y;
MathOp op = Add;
Console.WriteLine(op(3, 4)); // 7
op = Mul;
Console.WriteLine(op(3, 4)); // 12
MathOp 不关心方法叫什么,只在乎「两个 int 进、一个 int 出」。这和函数指针类似,但委托是类型安全的,签名对不上编译器直接拦下。
Note委托既能接方法组(
op = Add),也能接 Lambda(op = (x, y) => x + y)。两者在委托场景里经常混用。
为什么需要委托
它实现了晚绑定(late binding):写算法的人不写死具体逻辑,把「那一步」留给调用方。这是回调、事件、LINQ 的共同根基。学会了委托,后面三块都好懂。
适用场景
只要出现「我想把一段逻辑交给别人去决定、去调用」,就是委托的舞台:排序的比较规则、按钮被点击后的反应、把数据「怎么处理」交给调用方。它让方法能接收「另一个方法」当参数,极大提升复用度。
二、委托能装多个方法:多播
单个委托变量可以串起多个方法,用 += 追加,-= 移除:
namespace CSharpDemo;
void Hello() => Console.WriteLine("你好");
void Bye() => Console.WriteLine("再见");
Action greet = Hello;
greet += Bye;
greet(); // 你好 再见
greet -= Hello;
greet(); // 再见
这里 Action 是内置的「无参无返回值」委托(下一章细讲)。一次 greet() 会按添加顺序依次执行所有方法,这叫多播(multicast)。
Tip多播适合「一个动作触发一串反应」的场景,比如通知多个模块。它内部维护一个调用列表(invocation list)。
三、查看调用列表
想知道委托现在挂着哪些方法,可以拆开看:
namespace CSharpDemo;
void Hello() => Console.WriteLine("你好");
void Bye() => Console.WriteLine("再见");
Action greet = Hello;
greet += Bye;
foreach (var d in greet.GetInvocationList())
Console.WriteLine(d.Method.Name);
输出会是 Hello、Bye。这在调试「怎么多输出了东西」时很有用。
四、把委托当方法参数
委托最常见的用法是「作为方法的参数」,把行为传进去。比如一个通用计算器,运算符由调用方决定:
namespace CSharpDemo;
delegate int MathOp(int a, int b);
int Compute(int x, int y, MathOp op) => op(x, y);
int Add(int a, int b) => a + b;
int Mul(int a, int b) => a * b;
Console.WriteLine(Compute(3, 4, Add)); // 7
Console.WriteLine(Compute(3, 4, Mul)); // 12
Compute 不知道具体算什么,只负责「拿两个数调一下你给的方法」。这就是委托灵活的根源:算法骨架你写,关键一步交给别人。下一章的内置委托 Func 干的就是同一件事,只是不必自己声明类型。
五、多播的两个坑
第一个坑:带返回值的方法多播时,只保留最后一个方法的返回值,前面的全被丢弃:
namespace CSharpDemo;
delegate int GetNum();
int One() => 1;
int Two() => 2;
GetNum g = One;
g += Two;
Console.WriteLine(g()); // 2,One 的返回值被丢掉
所以如果多播,尽量让方法返回 void,别依赖返回值。
第二个坑:多播中某个方法抛异常,后续方法不再执行。链在出错处断掉,这是新手调试时最困惑的地方之一。
Warning多播链里只要有一个方法抛异常,整条链当场中断,后面的方法全都不跑。如果你希望「一个失败不影响其他」,就得自己用
GetInvocationList()遍历、逐个try/catch调用,而不能简单地greet()一下了事。
Note委托变量自己也能用
+和-组合出新的委托,效果和+=/-=等价,但+=更直观,日常就用它。
六、委托和事件的关系
你大概听说过「事件」。其实事件就是建立在委托之上的:它用 delegate 类型承载方法列表,但加了访问限制——外部只能 +=/-=,不能直接「赋值覆盖」或「调用」。下一章先讲内置委托,再后面专门讲事件。
Tip现在写代码,基本不用自己
delegate定义类型了——.NET提供了Action、Func等泛型委托。但理解delegate关键字,才能明白这些内置类型到底是什么。
七、与内置委托的对比
自己 delegate int MathOp(int a, int b); 和内置的 Func<int, int, int> 在「能装什么方法」上完全等价。区别只在名字:MathOp 语义更清楚(一看就知道是数学运算),Func<...> 更通用、不用额外声明。日常九成情况直接用内置委托;只有当「一个好名字本身就是文档」时(尤其事件),才自定义委托类型。
还有一点认知上的好处:用内置委托时,阅读代码的人不用跳去翻「这个 MathOp 到底长啥样」,一看 Func<int,int,int> 就懂是「两数进一数出」。自定义委托胜在「取名即文档」,内置委托胜在「零认知成本」。团队里通常会约定:事件和对外 API 才自定义,内部逻辑一律用内置。
八、最佳实践
- 需要自定义委托时,给它起个表意的名字(如
PriceChangedHandler),名字即文档。 - 多播优先用
void方法,别指望收集返回值。 - 多播链里要容错,就遍历调用列表逐个
try/catch。 - 日常优先用
Action/Func等内置委托,少自定义。 - 委托变量改用
+=/-=维护,别直接赋值覆盖整条链。
九、委托与接口:何时用哪个
有人会问:想把「一个行为」传进去,为啥不用接口?接口适合「一组相关方法」的契约,比如一个完整的日志器有 Log/Flush/Dispose 多个成员;委托适合「就一个方法」的灵活点。只传一个动作,委托更轻;要传一整套能力,接口更合适。两者不是二选一,而是按「粒度」取用:细到一个步骤用委托,粗到一整套职责用接口。
十、小结
委托把方法变成可传递的值,核心是「类型安全的晚绑定」。+= 把多个方法串成多播,一次调用全部触发。记住两条红线:多播返回值只看最后一个;中途异常会断链。下章看内置委托,日常开发几乎离不开它们。