Unity下C#静态Action委托订阅与自动取消订阅实现方案咨询
问题背景
我们基于Unity开发,项目里有一个包含静态Action的Listeners类,业务代码会在各处订阅这些事件:
class Listeners{ public static Action OnGameLoaded; public static Action OnAfterGameLoaded; }
原本每个需要订阅的类(比如Monster)都要手动编写重复代码:先取消旧订阅(避免重复绑定)、添加新订阅,还要在对象销毁时手动取消订阅,流程繁琐且容易遗漏:
class Monster : MonoBehaviour { // Awake在对象创建时调用 public override void Awake(){ Listeners.OnGameLoaded -= OnGameLoaded; // 先取消防止重复订阅 Listeners.OnGameLoaded += OnGameLoaded; } // OnDestroy在对象销毁时调用 public override void OnDestroy(){ Listeners.OnGameLoaded -= OnGameLoaded; } private void OnGameLoaded(){ // 业务逻辑实现 } }
我们希望简化流程:只需要调用一个Subscribe方法完成订阅,无需手动处理OnDestroy中的取消逻辑,理想用法如下:
class Monster : SomeSuperclass { public override void Awake(){ Subscribe(Listeners.OnGameLoaded, OnGameLoaded); } private void OnGameLoaded(){ // 业务逻辑实现 } // 无需再编写OnDestroy代码 }
最初尝试编写了一个基类来封装订阅逻辑,但调试发现:由于Action是值类型,传递时会被复制,导致基类存储的Action实例和Listeners类中的静态Action实例不一致,最终无法完成有效取消订阅。原基类代码如下:
class BaseClass : MonoBehaviour { private Dictionary<Action, Action> actions = new Dictionary<Action, Action>(); protected void Subscribe(ref Action action, Action function) { action += function; actions.Add(action, function); } public virtual void OnDestroy() { // 此处操作的是旧的Action实例,无法影响静态类中的当前Action foreach(KeyValuePair<Action, Action> entry in actions) { Action action = entry.Key; action -= entry.Value; } } }
解决方案
核心问题是不能存储Action值本身,而是要存储对原始静态Action的引用,或者直接存储“取消订阅”的操作逻辑,在对象销毁时执行。以下提供两种可行实现:
方式一:存储取消订阅操作(简洁版)
修改基类,不再存储Action实例,而是存储一个封装了取消订阅逻辑的委托,在OnDestroy时执行这些委托:
class BaseSubscriber : MonoBehaviour { private List<Action> _unsubscribers = new List<Action>(); protected void Subscribe(ref Action sourceAction, Action handler) { // 先取消再订阅,避免重复绑定 sourceAction -= handler; sourceAction += handler; // 存储取消订阅的操作 _unsubscribers.Add(() => sourceAction -= handler); } protected virtual void OnDestroy() { foreach (var unsubscribe in _unsubscribers) { unsubscribe(); } _unsubscribers.Clear(); } }
使用时直接继承该基类,调用Subscribe即可:
class Monster : BaseSubscriber { public override void Awake(){ Subscribe(ref Listeners.OnGameLoaded, OnGameLoaded); } private void OnGameLoaded(){ // 业务逻辑实现 } }
方式二:结合WeakReference(防内存泄漏版)
如果担心静态Action持有MonoBehaviour实例导致内存泄漏(比如对象已销毁但订阅未取消),可以用WeakReference包装实例,实现自动清理:
class SafeBaseSubscriber : MonoBehaviour { private List<Action> _unsubscribers = new List<Action>(); // 针对无参数Action的重载 protected void Subscribe(ref Action sourceAction, Action handler) { WeakReference<MonoBehaviour> weakRef = new WeakReference<MonoBehaviour>(this); Action wrappedHandler = () => { if (weakRef.TryGetTarget(out MonoBehaviour instance)) { handler(); } else { // 实例已销毁,自动从静态Action中移除 sourceAction -= wrappedHandler; } }; sourceAction -= wrappedHandler; sourceAction += wrappedHandler; _unsubscribers.Add(() => sourceAction -= wrappedHandler); } // 针对带参数Action的重载(可选) protected void Subscribe<T>(ref Action<T> sourceAction, Action<T> handler) where T : MonoBehaviour { WeakReference<T> weakRef = new WeakReference<T>(this as T); Action<T> wrappedHandler = (param) => { if (weakRef.TryGetTarget(out T instance)) { handler(param); } else { sourceAction -= wrappedHandler; } }; sourceAction -= wrappedHandler; sourceAction += wrappedHandler; _unsubscribers.Add(() => sourceAction -= wrappedHandler); } protected virtual void OnDestroy() { foreach (var unsubscribe in _unsubscribers) { unsubscribe(); } _unsubscribers.Clear(); } }
原代码无效的原因
Action本质是值类型委托,每次执行+=或-=都会生成一个新的Action实例。原基类中把修改后的Action实例存入字典,但Listeners类中的静态Action已经指向了新的实例,销毁时操作的是字典里的旧实例,自然无法影响到静态类中的当前Action,导致取消订阅失效。
内容的提问来源于stack exchange,提问作者Jacob

