C# 委托与事件:定义、用法与实现机制

一份系统梳理 C# 委托(Delegate)与事件(Event)的笔记,覆盖语法、典型用法、IL 层实现机制

一、委托(Delegate)

1.1 定义

委托是 C# 中的一种类型安全的函数指针,它封装了一个具有特定签名(参数列表和返回类型)的方法引用。委托本身是一个类,继承自 System.MulticastDelegate

// 声明一个委托类型
public delegate void MyAction(string message);

public delegate int MathOperation(int a, int b);

// C# 内置泛型委托(推荐使用,避免重复声明)
Action<string>      // 无返回值,1 个参数
Action<int, int>    // 无返回值,2 个参数
Func<int, int, int> // 有返回值(前几个是参数,最后一个是返回值)
Predicate<T>        // 返回 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, Func<int, 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) 多播委托(一个变量装多个方法,按顺序依次调用)
Action<string> logger = Console.WriteLine;
logger += msg => File.AppendAllText("log.txt", msg + "\n");
logger("hello"); // 同时打到控制台和写入文件

常见使用场景:回调、策略模式、LINQ、异步回调、事件。

二、事件(Event)

2.1 定义

事件是基于委托的受限封装,本质是"对委托字段的封装 + 一组访问限制"。

public class Button
{
    // 1) 声明事件(基于某个委托类型)
    public event Action OnClicked;

    // 2) 触发事件(只能在声明类内部触发)
    public void Click() => OnClicked?.Invoke();

    // 3) 提供订阅控制权(如订阅即通知 sender)
    public void SubscribeWithSender(Action<Button> 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 → M3

MulticastDelegate 内部用链表串起所有方法;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 泛型委托的实现

Action<T> / Func<T, R> 在 .NET 源码里都是泛型类,由 JIT 为每个不同类型参数单独生成代码。所以 Action<int>Action<string> 是两个不同的类型,节省运行时内存(不需要装箱)。

四、委托 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.Action<VRHand> 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 Func<float, float> intensityCurve = x => x; // 默认线性
    public void Play(float strength)
    {
        float v = Mathf.Clamp01(intensityCurve(strength));
        OVRInput.SetControllerVibration(v, v, OVRInput.Controller.RTouch);
    }
}

5.4 工程常见坑

  1. OnDisable / OnDestroy 一定要 -= 反订阅,否则事件持有 MonoBehaviour 引用导致内存泄漏 + 空引用回调
  2. 多播委托任一 handler 抛异常会中断后续,所以 UI 通知类建议每个 handler 自己 try-catch,或用 GetInvocationList() 手动遍历。
  3. 在 Unity Inspector 里要序列化委托时,用 UnityEvent(可序列化),普通 C# event 不能在 Inspector 显示。

六、一句话总结

  • 委托 = 类型安全的方法引用 + 多播链表,核心是把"行为"作为数据传递。
  • 事件 = 委托的"安全访问层":外部能订阅不能篡改,触发权牢牢握在声明方手里。
  • 实现机制delegate 是继承 MulticastDelegate 的类,event 是私有委托字段 + 编译器生成的原子 add/remove 方法。

更多推荐