一. 委托的本质
- 委托的使用(三步)
public delegate int CalculateDelegate(int a, int b);
CalculateDelegate calc = Add;
CalculateDelegate calc2 = new CalculateDelegate(Add);
int result = calc(3, 5);
static int Add(int a, int b) => a + b;
- 委托的本质
- 从两种创建实例的方式来看, 委托是一个函数指针, 也是一个类。实际上,声明一个委托之后, 编译器自动将委托转换成类的声明
public delegate void MyDelegate(int value);
public class MyDelegate : MulticastDelegate{}
private MyDelegate DoTest;
public void Test()
{
print(DoTest is MulticastDelegate);
DoTest = Test1;
print(DoTest is MulticastDelegate);
}
- 委托内部“持有”的是什么?(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;
Console.WriteLine(dmgDelegate.Target);
Console.WriteLine(dmgDelegate.Method);
GlobalEventManager.OnGameOver += () =>
{
this.gameObject.SetActive(false);
};
二. 多播委托
- 多播委托的使用和坑
+= / -= 维护调用链(Invocation List),调用时按订阅顺序依次执行
- 其中一个回调抛异常:若不捕获异常,后续回调一般不会继续执行(异常会中断调用链)
- 有返回值的多播委托:调用结果通常只会得到“最后一个订阅者”的返回值(其他返回值会被丢弃)
public delegate void OnPlayerDead()
// 声明
OnPlayerDead callback = null
// += 添加订阅
callback += PlayDeathAnimation
callback += ShowRespawnUI
callback += SaveGameData
// 调用时,按添加顺序依次执行
callback?.Invoke()
// -= 移除订阅
callback -= ShowRespawnUI
- Unity内置委托
Action
Action<int>
Action<int, string>
Action<int, string, bool>
Func<int>
Func<int, bool>
Func<int, string, float>
三.事件
- 关键字 —— event
- 与委托的区别与联系:
- 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
四. 闭包
- 定义: 当一个函数(lambda/匿名方法)在其原始作用域之外执行,但仍然能够访问并使用当初外层作用域的局部变量时,这种“函数 + 被捕获环境”的组合就是闭包
// 捕获了 i
for (int i = 0; i < buttons.Count; i++)
{
buttons[i].onClick.AddListener(() => OnClick(i));
}
// 捕获index
for (int i = 0
{
int index = i
buttons[i].onClick.AddListener(() => OnClick(index))
}
- 理解: 上述捕获, 实际上捕获的并不是变量的值, 而是捕获的引用(相当于指针, 即存放变量的地址), 所以上面第一段代码,捕获的i, 实际上捕获的同一个地址, 使用时, 只能去到一个i的值, 这里是3; 而后面一段代码, 则是捕获了N个index变量(声明了buttons.Count个index)
- 这也是闭包捕获this, 导致无法被GC释放, 所以要释放相应 订阅内容
GlobalEventManager.OnGameOver += () =>
{
this.gameObject.SetActive(false);
};