C#委托与事件:核心原理与实战解析

📅 2026/7/1 4:08:28
C#委托与事件:核心原理与实战解析
C# 委托与事件定义、用法与实现机制一份系统梳理 C# 委托Delegate与事件Event的笔记覆盖语法、典型用法、IL 层实现机制一、委托Delegate1.1 定义委托是 C# 中的一种类型安全的函数指针它封装了一个具有特定签名参数列表和返回类型的方法引用。委托本身是一个类继承自System.MulticastDelegate。// 声明一个委托类型 public delegate void MyAction(string message); public delegate int MathOperation(int a, int b); // C# 内置泛型委托推荐使用避免重复声明 Actionstring // 无返回值1 个参数 Actionint, int // 无返回值2 个参数 Funcint, int, int // 有返回值前几个是参数最后一个是返回值 PredicateT // 返回 bool 的单参数委托1.2 用法// 1) 定义方法 public class Calculator { public int Add(int a, int b) a b; public int Multiply(int a, int b) a * b; // 2) 方法可以像参数一样传递这就是委托的核心价值 public int Compute(int a, int b, Funcint, int, int op) op(a, b); } // 3) 使用 var calc new Calculator(); int sum calc.Compute(3, 5, calc.Add); // 3 5 8 int prod calc.Compute(3, 5, calc.Multiply); // 3 * 5 15 // 4) 多播委托一个变量装多个方法按顺序依次调用 Actionstring logger Console.WriteLine; logger msg File.AppendAllText(log.txt, msg \n); logger(hello); // 同时打到控制台和写入文件常见使用场景回调、策略模式、LINQ、异步回调、事件。二、事件Event2.1 定义事件是基于委托的受限封装本质是对委托字段的封装 一组访问限制。public class Button { // 1) 声明事件基于某个委托类型 public event Action OnClicked; // 2) 触发事件只能在声明类内部触发 public void Click() OnClicked?.Invoke(); // 3) 提供订阅控制权如订阅即通知 sender public void SubscribeWithSender(ActionButton handler) { OnClicked () handler(this); } } public class UI { void Start() { var btn new Button(); btn.OnClicked HandleClick; // 外部订阅OK // btn.OnClicked null; // 外部赋值编译错误事件保护 // btn.OnClicked.Invoke(); // 外部触发编译错误事件保护 } void HandleClick() Debug.Log(按钮被点击); }2.2 事件保护机制操作普通委托字段public Action X;事件public event Action X;外部obj.X null✅ 可以❌ 禁止外部obj.X(...)✅ 可以❌ 禁止外部obj.X ...✅ 可以✅ 可以内部X?.Invoke()✅ 可以✅ 可以这就是事件存在的最大意义把让别人订阅/取消订阅的权限开放但把赋值和调用的权限封死避免外部代码篡改触发逻辑。三、实现机制IL 层这是面试/底层最常被问的部分。3.1 委托的本质public delegate void MyDel(string s);C# 编译器在编译期会把它编译成一个继承自System.MulticastDelegate的类class MyDel : System.MulticastDelegate { public MyDel(object target, IntPtr method); // 构造包装目标对象 方法指针 public virtual void Invoke(string s); // 同步调用 public virtual IAsyncResult BeginInvoke(...);// 异步调用线程池 public virtual void EndInvoke(IAsyncResult); // 父类 MulticastDelegate 提供 // - invocationList : Delegate[] 持有所有订阅方法的链表 // - GetInvocationList() 返回方法链表 }一个Delegate实例内部有三个关键字段_targetobject方法所属的对象静态方法时为null_methodPtrIntPtr方法入口地址native function pointer_invocationListDelegate[]多播时链表的头/串联结构调用del.Invoke(args)实际就是通过方法指针跳到目标方法把参数和_target传过去。3.2 多播委托的实现Action a M1; // invocationList [M1] a M2; // invocationList [M1, M2]M1 的 _next 指向 M2 a M3; // invocationList [M1, M2, M3] a(); // 按顺序执行 M1 → M2 → M3MulticastDelegate内部用链表串起所有方法GetInvocationList()可以拆出单链。⚠️ 任何一个方法抛异常后续方法不会再执行这是常见坑要自己try-catch每个 handler。3.3 事件的本质编译器魔法public event Action OnClicked;编译器编译后实际生成的是这样的结构简化// 私有委托字段外部访问不到 private Action OnClicked; // 加锁的 add/remove 方法线程安全 [CompilerGenerated] public void add_OnClicked(Action handler) { Action oldHandler; Action newHandler; do { oldHandler this.OnClicked; newHandler (Action)Delegate.Combine(oldHandler, handler); } while (Interlocked.CompareExchange(ref this.OnClicked, newHandler, oldHandler) ! oldHandler); } [CompilerGenerated] public void remove_OnClicked(Action handler) { // 同理用 CompareExchange 做无锁线程安全移除 }关键点字段私有化→ 外部无法直接或Invoke只暴露同步的 add/remove不支持 removeAll、不支持直接 Invoke 整个链用Interlocked.CompareExchange做原子操作 → 多线程下订阅/取消订阅线程安全C# 8.0 还有event Action E1 { add { ... } remove { ... } }自定义访问器版本3.4 泛型委托的实现ActionT/FuncT, R在 .NET 源码里都是泛型类由 JIT 为每个不同类型参数单独生成代码。所以Actionint和Actionstring是两个不同的类型节省运行时内存不需要装箱。四、委托 vs 事件 对比速记维度delegate委托event事件本质一个类型类一个成员字段 访问器语义我可以把方法当数据传递我发布了通知订阅者可以监听谁能调用任何持有引用的人仅声明方谁能赋值是否谁能/-是是典型场景回调、策略、参数化行为UI 交互、状态变更通知、观察者默认 vs 推荐自定义场景多数场景优先用事件五、在 Unity项目里常见用法5.1 UnityEvent —— Inspector 友好的事件public Button button; public UnityEvent onGrabbed; void OnTriggerEnter(Collider c) { if (c.CompareTag(Hand)) onGrabbed?.Invoke(); }Inspector 里拖引用绑定策划友好。5.2 C# event —— 跨脚本逻辑解耦public class VRHand : MonoBehaviour { public static event System.ActionVRHand OnAnyHandGrabbed; void Grab() OnAnyHandGrabbed?.Invoke(this); } public class TutorialGuide : MonoBehaviour { void OnEnable() VRHand.OnAnyHandGrabbed Hint; void OnDisable() VRHand.OnAnyHandGrabbed - Hint; // 必须反订阅 void Hint(VRHand h) ShowTip(尝试用另一只手); }5.3 Func 委托 —— 用作可配置的算法策略public class HapticFeedback : MonoBehaviour { public Funcfloat, float intensityCurve x x; // 默认线性 public void Play(float strength) { float v Mathf.Clamp01(intensityCurve(strength)); OVRInput.SetControllerVibration(v, v, OVRInput.Controller.RTouch); } }5.4 工程常见坑OnDisable/OnDestroy一定要-反订阅否则事件持有MonoBehaviour引用导致内存泄漏 空引用回调。多播委托任一 handler 抛异常会中断后续所以 UI 通知类建议每个 handler 自己try-catch或用GetInvocationList()手动遍历。在 Unity Inspector 里要序列化委托时用UnityEvent可序列化普通 C#event不能在 Inspector 显示。六、一句话总结委托 类型安全的方法引用 多播链表核心是把行为作为数据传递。事件 委托的安全访问层外部能订阅不能篡改触发权牢牢握在声明方手里。实现机制delegate是继承MulticastDelegate的类event是私有委托字段 编译器生成的原子 add/remove 方法。