C# 中的委托与事件
委托是一种对方法的引用(类似指针),实例委托时,可以与任何具有相同签名的方法关联。
委托使用 delegate 关键字声明,在 delegate 前加上 event 后,则将委托声明为事件,实际上为委托加上更多的限制,事件只能在声明类中调用(发布),且只能使用 +=,-+ 运算符 订阅 / 取消 。
更新注意事项:在 Unity 中使用事件
- 同一方法可被重复订阅(重复进行 +=)造成方法多次调用,在重新订阅时确定先取消订阅。比较常见的是多次调用 OnEnable 造成重复订阅
- 订阅者被销毁后,如果未取消订阅,则发布者继续保持对该方法的引用(任然可以执行该方法),且造成订阅者不能被GC。
- 推荐做法:订阅者先于发布者销毁则应在订阅者销毁时取消订阅。如果在 OnEnable 中完成订阅,则应该在 OnDisable 中取消,避免 enable=true 造成重复订阅。订阅者不会被提前销毁或订阅者与发布者在同一 GameObject 具有相同的生命周期,可以使用 Start 方法进行订阅(只会执行一次订阅且不担心取消)
using UnityEngine;
// 发布类
public class DelegateScript : MonoBehaviour
{
// 定义委托
public delegate void SampleDelegate(int i);
// 声明委托
public SampleDelegate sampleDelegate;
// 将委托声明为事件
public event SampleDelegate sampleDelegateEvent;
private void Start()
{
// 发布委托
sampleDelegate?.Invoke(1);
// 发布事件,只能在本类中调用
sampleDelegateEvent?.Invoke(2);
}
}
using UnityEngine;
// 订阅类
public class DelegateTest : MonoBehaviour
{
public DelegateScript delegateScript; // 发布类
void Start()
{
// 实例化委托(关联方法)
delegateScript.sampleDelegate = Register;
// 这个动作则叫订阅事件,只能使用 +=
delegateScript.sampleDelegateEvent += Register;
// 委托可在其他类中调用
delegateScript.sampleDelegate(3);
// 而事件不能在发布类之外调用,下面这句将报错
delegateScript.sampleDelegateEvent(4);
}
void Register(int i)
{
Debug.Log($"delegate {i} invoked.");
}
}
在平常使用时,用不用 event 关键字区别不大,但在C#文档中定义事件专指使用 event 关键字的情况。
C# 内置委托 Action and Func
在 C# System 命名空间下,提供预定义的 Action, Func 可直接使用。
using System;
using UnityEngine;
// 发布类
public class DelegateScript : MonoBehaviour
{
// 预定义的 Action,无返回值
public event Action<int> action;
// 预定义的 Func,有返回值
public event Func<int, string> func;
// 预定义的 EventHandler
public event EventHandler<MyEventArgs> eventHandler;
private void Start()
{
action?.Invoke(1);
func?.Invoke(2);
eventHandler.Invoke(this, new MyEventArgs(3));
}
}
// 自定义事件数据
public class MyEventArgs : EventArgs
{
public int value;
public MyEventArgs(int value)
{
this.value = value;
}
}
using UnityEngine;
// 订阅类
public class DelegateTest : MonoBehaviour
{
public DelegateScript delegateScript; // 引用发布类
void Start()
{
delegateScript.action += Register;
delegateScript.func += FuncRegister;
delegateScript.eventHandler += EventHandlerRegister;
}
void Register(int i)
{
Debug.Log($"delegate {i} invoked.");
}
string FuncRegister(int i)
{
string result = $"delegate {i} invoked.";
Debug.Log(result);
return result;
}
void EventHandlerRegister(object sender, MyEventArgs e)
{
Debug.Log($"delegate {e.value} invoked.");
}
}
UnityAction
由 Unity 实现的预定义 delegate。和 Action, Func 类似。
UnityEvents
可通过 Editor 进行配置绑定事件,非常方便。
using UnityEngine.Events;
...
// 定义 UnityEvent
public UnityEvent onStateEnter;
void Start()
{
onStateEnter?.Invoke();
}
默认的 UnityEvent 只支持无参数方法,可自定义有参数的 UnityEvent
using UnityEngine.Events;
[System.Serializable]
public class UnityEventInt : UnityEvent<int> {}
调用 UnityEvent
...
public UnityEventInt onStateEnter;
void Start()
{
onStateEnter?.Invoke(10);
}
除了通过编辑器,也可用脚本方式绑定事件。
onStateEnter.AddListener(() => Debug.Log("Execute")); // 针对无参的 UnityEvent
Unity Messaging System
这是目前新的 UI 系统使用的消息系统(如 IPointerEnterHandler IDropHandler 等),用于取代传统的 SendMessage 方式。下面演示如何自定义消息,让状态机传递消息给目标对象。
首先,通过实现 IEventSystemHandler 定义一个新的消息接口
using UnityEngine;
using UnityEngine.EventSystems;
// 定义消息接口,必须实现 IEventSystemHandler 接口
public interface IStateEnter : IEventSystemHandler
{
// 目的是将状态机中的 AnimatorStateInfo 传递给目标对象
void OnStateEnter(AnimatorStateInfo stateInfo);
}
在状态机中发布消息
using UnityEngine.EventSystems;
public class SMB: StateMachineBehaviour
{
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// 通过 ExecuteEvents.Execute 工具函数来发布消息
// 第一个参数指定消息接收的对象
// 参数 x 映射前面泛型消息接口(<IStateEnter>),其他参数不重要
ExecuteEvents.Execute<IStateEnter>(animator.gameObject, null, (x, y) => x.OnStateEnter(stateInfo));
}
}
接收消息,只有在目标对象上实现了 IStateEnter 接口,才能接收消息
using UnityEngine;
public class Player : MonoBehaviour, IStateEnter
{
public void OnStateEnter(AnimatorStateInfo stateInfo)
{
Debug.Log(stateInfo.xxx);
}
}
使用 Messaging System 需要事先指定接收的对象,就如 UI EventSystem 通过 Raycast 找到对象后,再将 IDropHandler 的消息送到该对象。
解绑事件
当事件发布者被销毁时,将立即触发垃圾回收,而订阅者被销毁时,不会立即触发垃圾回收。所以,发布者和订阅者都会被销毁的情况下(如在同一对象上),可以不用管,发布者长存,而订阅者会被销毁的情况,需要显示解绑事件。