[C#/Unity] 匿名函數與一般函數使用上的一個區別

        今天我有一個關於匿名函數(Lambda expression)使用上的坑想分享一下。

        這個坑是在我早期開發《Covid Lifestyle》的時候踩到的,當時遊戲中原定還有一個道具收集的場景,而某些道具還是被藏在另外一些更表層的道具之下的,因此,被藏起來的道具,只有在其對應的外層道具有被點撃觸發後,它自身才會進行顯示;同理,當外層道具因被再次點撃時而進行關閉時,顯示出來的內層道具也要隨之進行隱藏。

        這些表層的道具,比如抽屜,也分別保存著關閉時和開啟時的圖片、位置信息。而由於內、外層的道具同樣附有Collider2D,因此內層的道具不能作為外層道具的子物體,否則IPointerDown將會同時觸發兩個物體,所以在Hierarchy面板上,內層和外層道具是在同一層級中,而不具有父子物體關係的。

       但同時,比如說抽屜裡的紙巾。當抽屜被打開時,紙巾也要顯示出來,而抽屜被關閉時,紙巾也要隱藏,但它們又不存在父子物體的關係,如果想要做到這一點,比較直接的方法是讓外層道具直接拿到內層道具的引用,然後對其進行SetActive的控制。不過由於我想盡可能讓道具自己只負責自身的交互和觸發事件,因此,我做了一個EventCenter的中介,裡面包含了添加事件、移除事件和廣播事件三個功能。道具在Awake()時將自身的某個方法通過EventCenter加到一個字典裡,然後在OnDestroy()時移除,然後讓其他腳本通過EventCenter去進行廣播,觸發這個方法。

       內層道具的對事件中心的具體調用如下圖:

        內層道具在Awake()時候通過EventCenter將自身的激活和失活的控制加到EventCenter裡一個以事件名稱枚舉為鍵,委托類型為值的字典裡,然後讓外層道具的觸發事件去對這個內層道具的激活狀態進行控制。

        最開始的時候,正如上圖的注釋部分,我是通過Lambda表達式去進行事件的添加的:

void Awake()

{

EventCenter.AddListener(EventTypes.ShowItem, () => gameObject.SetActive(true));

}

void OnDestroy()

{

EventCenter.RemoveListener(EventTypes.ShowItem, () => gameObject.SetActive(true));

}

        隨後,問題就出現了,這種寫法在第一次加載場景時是沒有問題的,事件也能順利的被添加。但是當重新加載場景之後,再嘗試去觸發事件,就會報出 「MissingReferenceException」的錯誤,如下圖:

        經過調試後,發現事件監聽的添加都是沒有問題且正常運行的。我就開始推測是不是函數本身的問題,然後我嘗試把() => gameObject.SetActive(true) 封裝成一個一般函數:

void Show()

{

gameObject.SetActive(true);

}

        並使用這個函數取代原來的Lambda表達式,一切即回復正常。

 

一點總結

        其實這個坑的問題就在於匿名函數本身。在首次加載時,匿名方法能得到正常加到Delegate中,也能正常卸載,也能正常使用;再次加載時,匿名方法雖然也可以正常通過EventCenter加到delegate中,但是匿名方法中指定的gameObject卻無法正常訪問,得出MissingReferenceError。

        雖然AddListener和RemoveListener時所使用的lambda表達式都是同樣的內容,但是在compiler看來,它們卻是兩個不同的函數。在.NET中,匿名函數的指針地址是隨機分配的,我在OnDestroy()時的RemoveListener其實只是移除了一個相同函數體的匿名函數,並不能清除原來被添加的那個匿名函數,除非我直接Clear掉整個委托。

        那既然移除事件失敗了,當我重新加載場景時,場景中的所有物體也會被銷毀然後重新生成。但事件中心中的事件指向仍然是銷毀之前的物體,理所當然地,當我嘗試觸發事件時,自然會報出MissingReferenceError錯誤。

        這也是lambda表達式與一般函數的不同之處,前者更多像是一個臨時的函數,它們基本是不會被重新被調用到的或者找到它們的位置的;而後者更多像是注冊了一個地址一樣,我們可以隨時根據名字找到這個函數的位置。