這篇在講什麼
這篇在記一個 Unity 和 C# 事件系統裡很常踩到的坑:用 lambda 訂閱事件很方便,但之後如果想用「看起來一樣」的 lambda 移除它,通常不會成功。
問題不在事件中心壞掉,而是在 delegate 的身分判定。
問題長什麼樣
很多人會自然地寫成這樣:
void Awake()
{
EventCenter.AddListener(EventTypes.ShowItem, () => gameObject.SetActive(true));
}
void OnDestroy()
{
EventCenter.RemoveListener(EventTypes.ShowItem, () => gameObject.SetActive(true));
}
表面上看起來,AddListener 和 RemoveListener 傳入的是同一段邏輯,但實際上它們不是同一個 delegate 實例。
結果就是:
- 訂閱成功
- 移除失敗
- 事件之後還是可能打到已經被銷毀的物件
為什麼看起來一樣,卻不是同一個 handler
lambda 不是靠文字內容判斷是否相同,而是看實際建立出來的 delegate 物件。
下面兩段雖然長得一樣:
() => gameObject.SetActive(true)
() => gameObject.SetActive(true)
但在執行時,它們通常是兩個不同的 delegate。
也就是說:
AddListener(...)加進去的是 ARemoveListener(...)傳進去的是 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 捕捉了
this、gameObject或區域變數 - 你之後需要精準解除某個監聽器
在這些情境下,匿名函式雖然短,但維護風險會明顯提高。
如果還是想用 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 不能用,而是你要分清楚:
- 只是想快速傳一段邏輯
- 還是需要之後再把它精準移除
如果是後者,優先順序通常會是:
- 直接用命名方法
- 或先把 lambda 存成欄位
不要假設兩段看起來一樣的 lambda 會被當成同一個 handler。
先記住的重點
- 兩段長得一樣的 lambda,不代表是同一個 delegate
- 事件解除訂閱時,必須傳回原本那個 delegate 實例
- 需要移除事件時,命名方法通常比匿名函式更安全
- 如果要用 lambda,先存起來,再用同一份實例解除