這篇在講什麼

這篇在記一個 Unity 和 C# 事件系統裡很常踩到的坑:用 lambda 訂閱事件很方便,但之後如果想用「看起來一樣」的 lambda 移除它,通常不會成功。

問題不在事件中心壞掉,而是在 delegate 的身分判定。

問題長什麼樣

很多人會自然地寫成這樣:

void Awake()
{
    EventCenter.AddListener(EventTypes.ShowItem, () => gameObject.SetActive(true));
}

void OnDestroy()
{
    EventCenter.RemoveListener(EventTypes.ShowItem, () => gameObject.SetActive(true));
}

表面上看起來,AddListenerRemoveListener 傳入的是同一段邏輯,但實際上它們不是同一個 delegate 實例。

結果就是:

  • 訂閱成功
  • 移除失敗
  • 事件之後還是可能打到已經被銷毀的物件

為什麼看起來一樣,卻不是同一個 handler

lambda 不是靠文字內容判斷是否相同,而是看實際建立出來的 delegate 物件。

下面兩段雖然長得一樣:

() => gameObject.SetActive(true)
() => gameObject.SetActive(true)

但在執行時,它們通常是兩個不同的 delegate。

也就是說:

  • AddListener(...) 加進去的是 A
  • RemoveListener(...) 傳進去的是 B

B 不是 A,所以事件系統當然找不到原本那個監聽器。

為什麼命名方法就沒事

如果改成一般方法:

void Awake()
{
    EventCenter.AddListener(EventTypes.ShowItem, ShowItem);
}

void OnDestroy()
{
    EventCenter.RemoveListener(EventTypes.ShowItem, ShowItem);
}

void ShowItem()
{
    gameObject.SetActive(true);
}

這樣可以正常移除,因為前後傳入的是同一個方法目標,delegate identity 可以對得上。

什麼情況下 lambda 特別容易出事

以下幾種情況要特別小心:

  • 訂閱和取消訂閱分別發生在不同生命週期函式
  • lambda 捕捉了 thisgameObject 或區域變數
  • 你之後需要精準解除某個監聽器

在這些情境下,匿名函式雖然短,但維護風險會明顯提高。

如果還是想用 lambda,正確做法是什麼

如果你真的想保留 lambda,至少要先把 delegate 存起來,之後用同一份實例解除訂閱:

private Action? _showItemHandler;

void Awake()
{
    _showItemHandler = () => gameObject.SetActive(true);
    EventCenter.AddListener(EventTypes.ShowItem, _showItemHandler);
}

void OnDestroy()
{
    if (_showItemHandler is not null)
    {
        EventCenter.RemoveListener(EventTypes.ShowItem, _showItemHandler);
    }
}

這樣前後用的是同一個 delegate 實例,才有辦法正確移除。

我自己的整理

這個問題的本質不是 lambda 不能用,而是你要分清楚:

  • 只是想快速傳一段邏輯
  • 還是需要之後再把它精準移除

如果是後者,優先順序通常會是:

  1. 直接用命名方法
  2. 或先把 lambda 存成欄位

不要假設兩段看起來一樣的 lambda 會被當成同一個 handler。

先記住的重點

  • 兩段長得一樣的 lambda,不代表是同一個 delegate
  • 事件解除訂閱時,必須傳回原本那個 delegate 實例
  • 需要移除事件時,命名方法通常比匿名函式更安全
  • 如果要用 lambda,先存起來,再用同一份實例解除