C# 委托/事件

6 阅读3分钟

一. 委托的本质

  1. 委托的使用(三步)
// ① 定义委托类型 
// delegate关键字
// 返回类型: int
// 输入类型(int, int)
public delegate int CalculateDelegate(int a, int b);  

// ② 创建委托实例 
CalculateDelegate calc = Add;           // 静态方法/实例方法 直接赋值
CalculateDelegate calc2 = new CalculateDelegate(Add); // 等价写法

// ③ 调用委托
int result = calc(3, 5);                // 等价于 calc.Invoke(3, 5)

// ---- 被绑定的方法 ----
static int Add(int a, int b) => a + b;
  1. 委托的本质
  • 从两种创建实例的方式来看, 委托是一个函数指针, 也是一个。实际上,声明一个委托之后, 编译器自动将委托转换成类的声明
public delegate void MyDelegate(int value);

// 编译器实际生成的是一个类
public class MyDelegate : MulticastDelegate{}

private MyDelegate DoTest;
public void Test()
{
    print(DoTest is MulticastDelegate);   // false  (DoTest == null, null is ... 结果都是 false)
    DoTest = Test1;
    print(DoTest is MulticastDelegate);   // true  (输出为True, 证明DoTest就是MulticastDelegate类对象)
}

  1. 委托内部“持有”的是什么?(Target / Method)
  • 绑定静态方法时:Target == null
  • 绑定实例方法时:Target == 实例引用(这会影响 GC:只要委托还活着,Target 通常也会被间接保持存活)
public class Player
{
    public string Name = "Hero";
    public void TakeDamage(int dmg) => Console.WriteLine($"{Name} 受到 {dmg} 伤害");
}

// 理解委托内部结构
var player = new Player();
Action<int> dmgDelegate = player.TakeDamage;

// 委托对象内部持有:
// ├── Target  = player 实例引用  (实例方法时非null, 静态方法时为null)
// └── Method  = TakeDamage 方法信息
Console.WriteLine(dmgDelegate.Target);  // 输出 Player 对象
Console.WriteLine(dmgDelegate.Method);  // 输出 Void TakeDamage(Int32)



GlobalEventManager.OnGameOver += () =>
{
    this.gameObject.SetActive(false);  // 捕获 this, 导致this对象不能被释放
};

二. 多播委托

  1. 多播委托的使用和坑
  • += / -= 维护调用链(Invocation List),调用时按订阅顺序依次执行
  • 其中一个回调抛异常:若不捕获异常,后续回调一般不会继续执行(异常会中断调用链)
  • 有返回值的多播委托:调用结果通常只会得到“最后一个订阅者”的返回值(其他返回值会被丢弃)
public delegate void OnPlayerDead();

// 声明
OnPlayerDead callback = null;

// += 添加订阅
callback += PlayDeathAnimation;
callback += ShowRespawnUI;
callback += SaveGameData;

// 调用时,按添加顺序依次执行
callback?.Invoke();

// -= 移除订阅
callback -= ShowRespawnUI;
  1. Unity内置委托
// Action 系列 — 无返回值
Action                          // 无参无返回值
Action<int>                     // 1个参数
Action<int, string>             // 2个参数
Action<int, string, bool>       // 3个参数
// ... 最多支持 16 个参数

// Func 系列 — 有返回值 (最后一个泛型参数是返回值类型)
Func<int>                       // 无参,返回 int
Func<int, bool>                 // 1个参数int,返回 bool
Func<int, string, float>        // 2个参数,返回 float

三.事件

  1. 关键字 —— event
  2. 与委托的区别与联系:
  • event是特殊的委托,本质上是对委托的访问权限收窄(封装)
  • 事件在类外部只允许 += / -=,不允许 = 覆盖、不允许直接 Invoke()
  • 事件只能在声明它的类内部触发
public class EventSystem
{
    // 普通委托作为公共字段 — 暴露太多权限
    public Action OnGameOver;  
}

// ❌ 委托的问题
var es = new EventSystem();
es.OnGameOver = null;          //  外部可以直接清空所有订阅
es.OnGameOver.Invoke();        //  外部可以直接触发事件
es.OnGameOver = SomeMethod;    //  外部可以直接覆盖(=赋值)

// ✅ 加上 event 关键字后
public event Action OnGameOver;

es.OnGameOver = null;          // ❌ 编译错误!
es.OnGameOver.Invoke();        // ❌ 编译错误!
es.OnGameOver = SomeMethod;    // ❌ 编译错误!
es.OnGameOver += SomeMethod;   // ✅ 只允许 += 和 -=

四. 闭包

  1. 定义: 当一个函数(lambda/匿名方法)在其原始作用域之外执行,但仍然能够访问并使用当初外层作用域的局部变量时,这种“函数 + 被捕获环境”的组合就是闭包
// 捕获了 i
for (int i = 0; i < buttons.Count; i++)
{
    buttons[i].onClick.AddListener(() => OnClick(i)); 
}
// 捕获index
for (int i = 0; i < buttons.Count; i++)
{
    int index = i;
    buttons[i].onClick.AddListener(() => OnClick(index)); // ✅
}
  1. 理解: 上述捕获, 实际上捕获的并不是变量的值, 而是捕获的引用(相当于指针, 即存放变量的地址), 所以上面第一段代码,捕获的i, 实际上捕获的同一个地址, 使用时, 只能去到一个i的值, 这里是3; 而后面一段代码, 则是捕获了N个index变量(声明了buttons.Count个index)
  • 这也是闭包捕获this, 导致无法被GC释放, 所以要释放相应 订阅内容
GlobalEventManager.OnGameOver += () =>
{
    this.gameObject.SetActive(false);  // 捕获 this, 导致this对象不能被释放
};