Skip to content

订阅和取消订阅

csharp-subscribe-unsubscribe-deep-dive

一句话理解

订阅就是:

事件 += 我的函数;

取消订阅就是:

事件 -= 我的函数;

你可以把事件理解成一张“方法清单”。 += 是把方法加入清单。 -= 是把方法从清单移除。 事件触发时,清单里的方法会依次执行。

最典型的 Unity 写法

发布者:

c
using System;
using UnityEngine;

public class PlayerHealth : MonoBehaviour
{
    public event Action<int> OnHpChanged;

    public void TakeDamage(int damage)
    {
        currentHp -= damage;
        OnHpChanged?.Invoke(currentHp);
    }

    private int currentHp = 100;
}

订阅者:

c
using UnityEngine;

public class HealthUI : MonoBehaviour
{
    public PlayerHealth player;

    private void OnEnable()
    {
        player.OnHpChanged += Refresh;
    }

    private void OnDisable()
    {
        player.OnHpChanged -= Refresh;
    }

    private void Refresh(int hp)
    {
        Debug.Log("刷新血量:" + hp);
    }
}

这里要分清 3 个角色:

PlayerHealth 是事件发布者。 HealthUI 是事件订阅者。 Refresh 是被登记进去的回调函数。

为什么要取消订阅?

因为事件会保存对方法的引用。 如果 HealthUI 已经关闭或销毁,但还挂在 PlayerHealth.OnHpChanged 里,后面玩家扣血时,它仍然可能被调用。

结果可能是:

UI 重复刷新。 音效播放多次。 对象销毁后还被访问。 旧对象无法及时释放。 出现空引用异常。

Unity 里常用配对

启用期间才监听:

c
OnEnable  订阅
OnDisable 取消订阅

整个对象生命周期都监听:

c
Awake 或 Start 订阅
OnDestroy 取消订阅

UI 按钮:

c
button.onClick.AddListener(OnClick);
button.onClick.RemoveListener(OnClick);

新手最容易踩的坑

第一,重复订阅。

c
player.OnDead += ShowGameOver;
player.OnDead += ShowGameOver;

这样事件触发时,ShowGameOver 会执行两次。

第二,取消的不是同一个方法。

c
button.onClick.AddListener(() => Debug.Log("点击"));
button.onClick.RemoveListener(() => Debug.Log("点击"));

这两段 lambda 看起来一样,但通常不是同一个回调实例,所以移除可能失败。需要移除时,优先用具名方法:

c
button.onClick.AddListener(OnClick);
button.onClick.RemoveListener(OnClick);

void OnClick()
{
    Debug.Log("点击");
}

第三,长寿命对象引用短寿命对象。

比如全局管理器、单例、静态事件很容易活得比普通场景对象久。 订阅这类事件时,取消订阅一定要非常明确。

参考链接:

Microsoft - EventsMicrosoft - DelegatesMicrosoft - event keywordUnity - UnityEvent.AddListenerUnity - UnityEvent.RemoveListenerUnity - MonoBehaviour.OnEnableUnity - MonoBehaviour.OnDisable

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0