委托与事件:从类型安全函数指针到发布-订阅模型
一、 委托的核心本质:它不是方法,而是“类型契约”
1. 声明委托(定义契约)
在 C# 中,定义一个委托类型需要严格指定其参数类型和返回类型。委托是一个引用类型,它在底层本质上是一个派生自System.MulticastDelegate 的类(Class)。这就像是定义了一个方法的“形状”或契约:
1 | public delegate int MathOperation(int num1, int num2); |
当你敲下上面这行代码时,C# 编译器在底层会悄悄帮你生成一个完整的类:
1 | // 编译器的幕后黑手:所有的委托类型都隐式派生自 System.MulticastDelegate |
因为委托本质上是类,所以类能声明在哪里(比如命名空间下、类内部),委托就可以声明在哪里。当把方法赋值给委托时,就相当于声明:“该方法符合此契约要求。”
二、 自定义委托的“黄金四步法”与核心实操
手写一个自定义委托并在代码里跑起来,雷打不动分为四个核心步骤:
- 声明委托(定义契约) —— 规定这个委托只能指向什么参数和返回值的方法。
- 准备目标方法—— 手写具体的方法,静态方法或实例方法皆可。
- 实例化并绑定方法 —— 把具体方法塞给委托实例。
- 调用委托 —— 执行委托,间接触发被绑定的具体方法。
1. 基础演练:同步调用(绑定静态与实例方法)
1 | using System; |
三、 为什么要用委托?从“游戏按钮”看解耦的精髓
如果界面上有许多不同的按钮,点击按钮 1 要“开始游戏”,点击按钮 2 要“好友分享”。普通的死板写法是为每个按钮写一个专用的类,导致代码极度臃肿。
通过委托,我们可以实现完美的方法解耦,让按钮的类结构复用,而点击后的回调行为完全由外部决定。
1 | public class Button |
四、 多播委托(Multicast)的数组链条与致命暗坑
由于定义的委托类型都继承自 MulticastDelegate,它们天然都是多播委托。委托多播允许在一个委托中注册多个方法,内部维护了一个方法执行链,通过 += 和 -= 操作符进行管理。
1 | public class Zhang |
多播委托的致命应用坑点:
- 调用顺序:方法按照它们被添加的顺序依次调用。
- 异常处理(崩断):如果链条中某个方法抛出了异常,整个执行链会直接中断,后续方法不会被调用。
- 返回值被吞:如果委托有返回值,默认直接调用多播委托,只有最后一个方法的返回值会被保留并返回。
- 规范解法:如果需要拿到每一个回调方法的执行返回值,必须使用
GetInvocationList()获取子委托数组进行循环遍历调用:1
2
3
4
5
6
7Delegate[] delegates = cal.GetInvocationList();
for (int i = 0; i < delegates.Length; i++)
{
CalculateDelegate c = (CalculateDelegate)delegates[i];
int result = c(1, 3); // 逐个执行并稳稳拿到每一个返回值
Console.WriteLine(result);
}
- 规范解法:如果需要拿到每一个回调方法的执行返回值,必须使用
五、 全面拥抱三大内置通用泛型委托
在现代 C# 开发中,为了不让代码里充斥着密密麻麻的自定义 public delegate 声明,官方内置了三个通用的泛型委托,能够直接覆盖 99% 的声明需求:
- Action\<…\>:无返回值的方法类型,支持 0 到 16 个泛型输入参数。
- Func\<…\>:带返回值的方法类型,支持 0 到 16 个输入参数,最后一个泛型参数固定代表返回值的类型。
- Predicate:专门用于条件判断的泛型委托,输入一个指定对象,固定返回 bool。常用于集合筛选(如 List.Find、Array.Exists)。
六、 事件(Event):套在委托外面的“安全保护壳”
1. 为什么有了委托,还要发明事件?
在前面的“游戏按钮”案例里,因为 onClick 是个普通的委托变量,外部代码可以直接写 gameStartButton.onClick = null;。这一行赋值会直接把别人之前用 += 绑定的所有回调一刀切全部清空。甚至外部可以直接调用 gameStartButton.onClick(); 去强行触发点击。
这破坏了面向对象的封装和安全原则。为此,C# 提供了 event 关键字。事件本质上是一种特殊的多播委托,它基于发布-订阅模型,是只允许在定义类的内部触发的安全封装壳。
2. 标准的规范事件模型(EventHandler)
在企业级或上位机底层框架开发中,触发事件通常要遵循标准的 .NET 规范,使用内置的 EventHandler:
1 | // 1. 自定义事件参数(传递额外数据) |
3. 事件与委托的终极区别(面试高频)
| 特性 | 委托(Delegate) | 事件(Event) |
|---|---|---|
| 基础概念 | 自自定义引用类型,代表方法指针契约。 | 特殊的多播委托,基于发布-订阅机制。 |
| 外部赋值 (=) | 自由允许,但极易覆盖掉其他绑定的方法。 | 严厉禁止!外部只能使用+=和-=。 |
| 外部触发 (Invoke) | 自由允许,只要持有该委托变量就能随时调用。 | 严厉禁止!只有定义该事件的类内部才能触发。 |
| 设计目的 | 用于底层通用方法指针封装或灵活的回调传递。 | 提供安全的观察者模式,防范外部误操作。 |
七、 个人复盘心得
- 要防范事件引起的“内存泄漏”:
在实际开发中,只要执行了Publisher.PriceChanged += Subscriber.PrintOut;,发布者内部就会持有一个订阅者的长引用。如果订阅者生命周期结束了(例如窗体关闭),但如果没有显式调用-= 注销事件,垃圾回收器(GC)就永远无法回收它,导致内存积压。切记:有加必有减,对象销毁前一定要主动解绑事件! - 签名契约必须严丝合缝:
无论是自定义委托还是内置委托,参数个数、类型和返回值必须与目标方法百分之百匹配,任何细微的不一致都会引发编译错误。 - 调用前必须进行可空校验:
如果一个委托变量或者事件没有被任何外部方法订阅,它的值就是null。此时直接调用会产生NullReferenceException导致系统崩溃。在日常编写代码时,必须强制养成使用?.Invoke() 安全调用的肌肉记忆。






