Unity中使用Delegate, Action, Func, Reflection, UnityAction動態調用函數優劣對比
概述
在游戲開發中(其實別的領域也一樣啦),為了保證代碼框架的靈活,低耦合,拓展性強,許多時候需要動態地調用函數。在C++
里面,一種特殊的指針,函數指針,承擔了傳遞函數的功能。Javascript
里也有高級函數這么一說來將函數也作為參數使用。C#
作為一門擁有許多特性的現代編程語言來說,提供了多種解決方法。
比較
使用Delegate機制
Delegate
,直接翻譯就是委托,本質上是像C++
一樣傳遞一個函數指針,通過delegate
關鍵字聲明,同時也要標注出返回值類型和參數類型。它的本質其實是一個包含指向特定函數的對象而不是一個函數的引用,正是因為這一特點,我們才能將他作為參數傳遞,由此也可以說嗎,C#
仍然是一門強類型,靜態編譯的語言。然而由於創建一個Delegate
需要聲明的內容過多,C#
的System
庫里又提供了兩種方便的簡化創建方式,即Action
委托類和Func
委托類。這兩個東西仍然是委托,也就是說是對象,只不過各自省略了一些條件。Action
可以為所有沒有返回值類型的函數提供委托,通過范型參數對委托的輸入參數類型進行限制。而Func
委托則將返回值類型也使用泛型參數確定。由於這兩個簡化的委托都需要使用泛型參數來約束條件,所以只能通過Overload來提供不同參數長度的委托,並且System
庫里最多只給輸入參數重載到了16個(Func
還多一個給返回值參數用)。
由於其函數指針的本質,Invoke委托的方法並沒有很大的開銷,等同於call了一個虛函數,是call一個非虛函數開銷的1.5~2倍。虛函數就是指那些被virtual
修飾的函數,包括實現的接口里的所有函數和所有override
。增加的開銷是因為運行的時候進程需要查詢一張函數表來找到該函數的位置,並不顯著(絕對值太小)。一般來說,Delegate
和Event
系統配合起來是大多數人采取的解決方案。
然而委托也有缺點,委托對象需要在編譯前就進行賦值(如Action action = Method;
),實際上在編譯的時候編譯器對代碼進行了再翻譯,可以認為有一層很淺的語法糖。所以委托雖然可以動態調用函數,但是不能動態賦值。其次,如果使用匿名函數對委托進行賦值,GC的處理會比較難以預料。雖然方表示當委托對象賦值null
的時候,系統會自動銷毀匿名函數,不過據說不太靠譜。
使用Reflection機制
Reflection
,也就是使用C#
的反射來調用函數。一般是通過對象的GetType().GetMethod(xxx)
之類的手段調用,獲得一個MethodInfo
的對象,包括了函數名,參數表等等對象。反射本質是加載相關的進程集,然后查詢相關的類的元數據(Field
域,Method
方法等等),在再到對應實例化對象如果是非靜態方法的話。從我的描述就可以看出,這是一個相當復雜的過程,尤其牽扯到多個表的查詢和字符串比對,性能非常低下,同時由於存儲着更多的信息(比起委托的函數指針而言),內存占用也高。
不過反射真的就沒有啥好處嗎?作為一名IB學生,既然學了ToK,我們就要會使用批判性思維,從事物的兩面來考慮問題。由於反射查詢了指點的類的所有描述屬性,我們可以真正做到動態指定函數(使用string傳遞函數名)。除此之外,反射還可以獲取並調用私有API
。想想看,當你用了某個把重要底層邏輯私有掉的dll庫的時候,使用反射就可以輕而易舉獲取到這些內容,同樣有些庫的新特性可能有於不穩定而用private暫時禁止開發者使用,用Reflection
就可以提前看到這些東西啦,是不是非常一顆賽艇。
那么反射的效率該怎么提高呢?一種是使用il.Emit()
函數動態生成相關代碼,這樣再次調用這段代碼,就可以或得近似原生編譯出來的效果了。做法就不詳細介紹了,因為這不是我個人推薦的一種。通過這種方法優化,性能大概比編譯代碼慢20倍的樣子,並不是最理想的。那么,我們該如何進一步解決這個問題呢?答案是用Delegate.CreateDelegate()
這個靜態函數。這個函數允許你輸入一個MethodInfo
對象來創建一個委托類型對象。如果你再傳入函數所屬的類型實例,就可以獲得一個非靜態的方法的委托,這樣性能就提升到了委托的層次,同時也可以進行動態賦值。
使用UnityAction
UnityAction
是Unity3D自帶庫的一個類,是其對Action
的一種實現,大致上和System
的Action
並無區別,主要是為了能在Inspector
面板上編輯而做的序列化處理,這樣當有一個public
的UnityEvent
對象時,就可以在面板上添加刪除UnityAction
了。不過經過測試之后UnityAction
的效率不得不讓人吐槽,差不多比委托機制要慢4~5倍、、、明明都是基於委托的呀,非常神奇。Unity
目前的NGUI
,UGUI
里的EventSystem
都是基於這個機制實現的,可以從側面說明為什么Unity
的原生UI庫效率不高了(笑)。Anyway,我自己都是使用System.Action
來解決問題的,那么如何在inspector
上顯示就成了一個問題,我最后通過反射獲取給定腳本的所以方法實現了這一個,具體內容后面再解釋吧。
總結
進程編寫的靈活性與性能始終是互逆的,在這種情況下我們只能根據需要進行取舍。比如游戲運行時對性能要求高,那么就必須得用Delegate
,而在編輯器顯示的時候,為了動態獲取未知腳本的所有符合條件的函數,就不得不要反射機制,這個時候對性能要求就沒那么高了。為了連接這兩種不一樣的需求,我們又使用Delegate.CreateDelegate()
方法進行轉換,總而言之,不能一味地需一種用法就什么情況下都用,靈活地變化是必不可少的