首页 / C# 入门教程 / 委托基础与多播

C# 入门教程

委托基础与多播

本教程共 100 篇 · 第 54 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

C#C# 入门教程编程语言委托delegate多播

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);

输出会是 HelloBye。这在调试「怎么多输出了东西」时很有用。

四、把委托当方法参数

委托最常见的用法是「作为方法的参数」,把行为传进去。比如一个通用计算器,运算符由调用方决定:

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 提供了 ActionFunc 等泛型委托。但理解 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 多个成员;委托适合「就一个方法」的灵活点。只传一个动作,委托更轻;要传一整套能力,接口更合适。两者不是二选一,而是按「粒度」取用:细到一个步骤用委托,粗到一整套职责用接口。

十、小结

委托把方法变成可传递的值,核心是「类型安全的晚绑定」。+= 把多个方法串成多播,一次调用全部触发。记住两条红线:多播返回值只看最后一个;中途异常会断链。下章看内置委托,日常开发几乎离不开它们。