因爲工做繁忙因此距離上一篇博客已通過去一個多月的時間了,所以決心這個週末不管如何也得寫點東西出來,既是總結也是分享。那麼本文主要的內容集中在了委託的使用以及內部結構(固然還有事件了,可是受制於篇幅故分爲兩篇文章)以及結合一部分Unity3D的設計思考。固然因爲時間倉促,文中不免有一些疏漏和不許確,也歡迎各位指出,共同進步。編程
在設計模式中,有一種咱們經常會用到的設計模式——觀察者模式。那麼這種設計模式和咱們的主題「如何在Unity3D中使用委託」有什麼關係呢?別急,先讓咱們來聊一聊什麼是觀察者模式。設計模式
首先讓咱們來看看報紙和雜誌的訂閱是怎麼一回事:數組
若是各位讀者能看明白我上面所說的報紙和雜誌是如何訂閱的,那麼各位也就瞭解了觀察者模式究竟是怎麼一回事。除了名稱不大同樣,在觀察者模式中,報社或者說出版者被稱爲「主題」(Subject),而訂閱者則被稱爲「觀察者」(Observer)。將上面的報社和訂閱者的關係移植到觀察者模式中,就變成了以下這樣:主題(Subject)對象管理某些數據,當主題內的數據改變時,便會通知已經訂閱(註冊)的觀察者,而已經註冊主題的觀察者此時便會收到主題數據改變的通知並更新,而沒有註冊的對象則不會被通知。安全
當咱們試圖去勾勒觀察者模式時,可使用報紙訂閱服務,或者出版者和訂閱者來比擬。而在實際的開發中,觀察者模式被定義爲了以下這樣:架構
觀察者模式:定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的全部依賴者都會收到通知並自動更新。ide
那麼介紹了這麼多觀察者模式,是否是也該說一說委託了呢?是的,C#語言經過委託來實現回調函數的機制,而回調函數是一種頗有用的編程機制,能夠被普遍的用在觀察者模式中。函數
那麼Unity3D自己是否有提供這種機制呢?答案也是確定的,那麼和委託又有什麼區別呢?下面就讓咱們來聊一聊這個話題。性能
固然,不能否認Unity3D遊戲引擎的出現是遊戲開發者的一大福音。但不得不說的是,Unity3D的遊戲腳本的架構中是存在一些缺陷的。一個很好的例子就是本節要說的圍繞SendMessage和BroadcastMessage而構建的消息系統。之因此說Unity3D的這套消息系統存在缺陷,主要是因爲SendMessage和BroadcastMessage過於依賴反射機制(reflection)來查找消息對應的回調函數。頻繁的使用反射天然會影響性能,可是性能的損耗還並不是最爲嚴重的問題,更加嚴重的問題是使用這種機制以後代碼的維護成本。爲何說這樣作是一個很糟糕的事情呢?由於使用字符串來標識一個方法可能會致使不少隱患的出現。舉一個例子:假如開發團隊中某個開發者決定要重構某些代碼,很不巧,這部分代碼即是那些可能要被這些消息調用的方法定義的代碼,那麼若是方法被從新命名甚至被刪除,是否會致使很嚴重的隱患呢?答案是yes。這種隱患的可怕之處並不在於可能引起的編譯時錯誤,偏偏相反,這種隱患的可怕之處在於編譯器可能都不會報錯來提醒開發者某些方法已經被更名甚至是不存在了,面對一個可以正常的運行程序而沒有警覺是最可怕的,而何時這個隱患會爆發呢?就是觸發了特定的消息而找不到對應的方法的時候 ,但這時候發現問題所在每每已經太遲了。this
另外一個潛在的問題是因爲使用了反射機制於是Unity3D的這套消息系統也可以調用聲明爲私有的方法的。可是若是一個私有方法在聲明的類的內部沒有被使用,那麼正常的想法確定都認爲這是一段廢代碼,由於在這個類的外部不可能有人會調用它。那麼對待廢代碼的態度是什麼呢?我想不少開發者都會選擇消滅這段廢代碼,那麼一樣的隱患又會出現,可能在編譯時並無問題,甚至程序也能正常運行一段時間,可是隻要觸發了特定的消息而沒有對應的方法,那即是這種隱患爆發的時候。於是,是時候向Unity3D中的SendMessage和BroadcastMessage說拜拜了,讓咱們選擇C#的委託來實現本身的消息機制吧。spa
在非託管代碼C/C++中也存在相似的回調機制,可是這些非成員函數的地址僅僅是一個內存地址。而這個地址並不攜帶任何額外的信息,例如函數的參數個數、參數類型、函數的返回值類型,於是咱們說非託管C/C++代碼的回調函數不是類型安全的。而C#中提供的回調函數的機制即是委託,一種類型安全的機制。爲了直觀的瞭解委託,咱們先來看一段代碼:
using UnityEngine; using System.Collections; public class DelegateScript : MonoBehaviour { //聲明一個委託類型,它的實例引用一個方法 internal delegate void MyDelegate(int num); MyDelegate myDelegate; void Start () { //委託類型MyDelegate的實例myDelegate引用的方法 //是PrintNum myDelegate = PrintNum; myDelegate(50); //委託類型MyDelegate的實例myDelegate引用的方法 //DoubleNum myDelegate = DoubleNum; myDelegate(50); } void PrintNum(int num) { Debug.Log ("Print Num: " + num); } void DoubleNum(int num) { Debug.Log ("Double Num: " + num * 2); } }
下面咱們來看看這段代碼作的事情。在最開始,咱們能夠看到internal委託類型MyDelegate的聲明。委託要肯定一個回調方法簽名,包括參數以及返回類型等等,在本例中MyDelegate委託制定的回調方法的參數類型是int型,同時返回類型爲void。
DelegateScript類還定義了兩個私有方法PrintNum和DoubleNum,它們的分別實現了打印傳入的參數和打印傳入的參數的兩倍的功能。在Start方法中,MyDelegate類的實例myDelegate分別引用了這兩個方法,而且分別調用了這兩個方法。
看到這裏,不知道各位讀者是否會產生一些疑問,爲何一個方法可以像這樣myDelegate = PrintNum; 「賦值」給一個委託呢?這便不得不提C#2爲委託提供的方法組轉換。回溯C#1的委託機制,也就是十分原始的委託機制中,若是要建立一個委託實例就必需要同時指定委託類型和要調用的方法(執行的操做),於是剛剛的那行代碼就要被改成:
new MyDelegate(PrintNum);
即使回到C#1的時代,這行建立新的委託實例的代碼看上去彷佛並無讓開發者產生什麼很差的印象,可是若是是做爲較長的一個表達式的一部分時,就會讓人感受很冗繁了。一個明顯的例子是在啓動一個新的線程時候的表達式:
Thread th = new Thread(new ThreadStart(Method));
這樣看起來,C#1中的方式彷佛並不簡潔。於是C#2爲委託引入了方法組轉換機制,即支持從方法到兼容的委託類型的隱式轉換。就如同咱們一開始的例子中作的那樣。
//使用方法組轉換時,隱式轉換會將 //一個方法組轉換爲具備兼容簽名的 //任意委託類型 myDelegate = PrintNum; Thread th = new Thread(Method);
而這套機制之因此叫方法組轉換,一個重要的緣由就是因爲重載,可能不止一個方法適用。例以下面這段代碼所演示的那樣:
using UnityEngine; using System.Collections; public class DelegateScript : MonoBehaviour { //聲明一個委託類型,它的實例引用一個方法 delegate void MyDelegate(int num); //聲明一個委託類型,它的實例引用一個方法 delegate void MyDelegate2(int num, int num2); MyDelegate myDelegate; MyDelegate2 myDelegate2; void Start () { //委託類型MyDelegate的實例myDelegate引用的方法 //是PrintNum myDelegate = PrintNum; myDelegate(50); //委託類型MyDelegate2的實例myDelegate2引用的方法 //PrintNum的重載版本 myDelegate2 = PrintNum; myDelegate(50, 50); } void PrintNum(int num) { Debug.Log ("Print Num: " + num); } void PrintNum(int num1, int num2) { int result = num1 + num2; Debug.Log ("result num is : " + result); } }
這段代碼中有兩個方法名相同的方法:
void PrintNum(int num)
void PrintNum(int num1, int num2)
那麼根據方法組轉換機制,在向一個MyDelegate或一個MyDelegate2賦值時,均可以使用PrintNum做爲方法組(此時有2個PrintNum,於是是「組」),編譯器會選擇合適的重載版本。
固然,涉及到委託的還有它的另一個特色——委託參數的逆變性和委託返回類型的協變性。這個特性在不少文章中也有過介紹,可是這裏爲了使讀者更加加深印象,於是要具體的介紹一下委託的這種特性。
在爲委託實例引用方法時,C#容許引用類型的協變性和逆變性。協變性是指方法的返回類型能夠是從委託的返回類型派生的一個派生類,也就是說協變性描述的是委託返回類型。逆變性則是指方法獲取的參數的類型能夠是委託的參數的類型的基類,換言之逆變性描述的是委託的參數類型。
例如,咱們的項目中存在的基礎單位類(BaseUnitClass)、士兵類(SoldierClass)以及英雄類(HeroClass),其中基礎單位類BaseUnitClass做爲基類派生出了士兵類SoldierClass和英雄類HeroClass,那麼咱們能夠定義一個委託,就像下面這樣:
delegate Object TellMeYourName(SoldierClass soldier);
那麼咱們徹底能夠經過構造一個該委託類型的實例來引用具備如下原型的方法:
string TellMeYourNameMethod(BaseUnitClass base);
在這個例子中,TellMeYourNameMethod方法的參數類型是BaseUnitClass,它是TellMeYourName委託的參數類型SoldierClass的基類,這種參數的逆變性是容許的;而TellMeYourNameMethod方法的返回值類型爲string,是派生自TellMeYourName委託的返回值類型Object的,於是這種返回類型的協變性也是容許的。可是有一點須要指出的是,協變性和逆變性僅僅支持引用類型,因此若是是值類型或void則不支持。下面咱們接着舉一個例子,若是將TellMeYourNameMethod方法的返回類型改成值類型int,以下:
int TellMeYourNameMethod(BaseUnitClass base);
這個方法除了返回類型從string(引用類型)變成了int(值類型)以外,什麼都沒有被改變,可是若是要將這個方法綁定到剛剛的委託實例上,編譯器會報錯。雖然int型和string型同樣,都派生自Object類,可是int型是值類型,於是是不支持協變性的。這一點,各位讀者在實際的開發中必定要注意。
好了,到此咱們應該對委託有了一個初步的直觀印象。在本節中我帶領你們直觀的認識了委託如何在代碼中使用,以及經過C#2引入的方法組轉換機制爲委託實例引用合適的方法以及委託的協變性和逆變性。那麼本節就到此結束,接下來讓咱們更進一步的探索委託。
讓咱們從新定義一個委託並建立它的實例,以後再爲該實例綁定一個方法並調用它:
internal delegate void MyDelegate(int number); MyDelegate myDelegate = new MyDelegate(myMethod1); myDelegate = myMethod2; myDelegate(10);
從表面看,委託彷佛十分簡單,讓咱們拆分一下這段代碼:用C#中的delegate關鍵字定義了一個委託類型MyDelegate;使用new操做符來構造一個MyDelegate委託的實例myDelegate,經過構造函數建立的委託實例myDelegate此時所引用的方法是myMethod1,以後咱們經過方法組轉換爲myDelegate綁定另外一個對應的方法myMethod2;最後,用調用方法的語法來調用回調函數。看上去一切都十分簡單,但實際狀況是這樣嗎?
事實上編譯器和Mono運行時在幕後作了大量的工做來隱藏委託機制實現的複雜性。那麼本節就要來揭開委託究竟是如何實現的這個謎題。
下面讓咱們把目光從新聚焦在剛剛定義委託類型的那行代碼上:
internal delegate void MyDelegate(int number);
這行對開發者們來講十分簡單的代碼背後,編譯器爲咱們作了哪些幕後的工做呢?
讓咱們使用Refactor反編譯C#程序,能夠看到以下圖的結果:
能夠看到,編譯器實際上爲咱們定義了一個完整的類MyDelegate:
internal class MyDelegate : System.MulticastDelegate { //構造器 [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)] public MyDelegate(object @object, IntPtr method); // Invoke這個方法的原型和源代碼指定的同樣 [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)] public virtual void Invoke(int number); //如下的兩個方法實現對綁定的回調函數的一步回調 [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)] public virtual IAsyncResult BeginInvoke(int number, AsyncCallback callback, object @object); [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)] public virtual void EndInvoke(IAsyncResult result); }
能夠看到,編譯器爲咱們的MyDelegate類定義了4個方法:一個構造器、Invoke、BeginInvoke以及EndInvoke。而MyDelegate類自己又派生自基礎類庫中定義的System.MulticastDelegate類型,因此這裏須要說明的一點是全部的委託類型都派生自System.MulticastDelegate。可是各位讀者可能也會了解到在C#的基礎類庫中還定義了另一個委託類System.Delegate,甚至System.MulticastDelegate也是從System.Delegate派生而來,而System.Delegate則繼承自System.Object類。那麼爲什麼會有兩個委託類呢?這實際上是C#的開發者留下的歷史遺留問題,雖然全部咱們本身建立的委託類型都繼承自MulticastDelegate類,可是仍然會有一些Delegate類的方法會被用到。最典型的例子即是Delegate類的兩個靜態方法Combine和Remove,而這兩個方法的參數都是Delegate類型的。
public static Delegate Combine( Delegate a, Delegate b ) public static Delegate Remove( Delegate source, Delegate value )
因爲咱們定義的委託類派生自MulticastDelegate而MulticastDelegate又派生自Delegate,於是咱們定義的委託類型能夠做爲這兩個方法的參數。
再回到咱們的MyDelegate委託類,因爲委託是類,於是凡是可以定義類的地方,均可以定義委託,因此委託類既能夠在全局範圍中定義,也能夠嵌套在一個類型中定義。一樣,委託類也有訪問修飾符,既能夠經過指定委託類的訪問修飾符例如:private、internal、public等等來限定訪問權限。
因爲全部的委託類型都繼承於MulticastDelegate類,於是它們也繼承了MulticastDelegate類的字段、屬性以及方法,下面列出三個最重要的非公有字段:
字段 |
類型 |
做用 |
_target |
System.Object |
當委託的實例包裝一個靜態方法時,該字段爲null;當委託的實例包裝的是一個實例方法時,這個字段引用的是回調方法要操做的對象。也就是說,這個字段的值是要傳遞給實例方法的隱式參數this。 |
_methodPtr |
System.IntPtr |
一個內部的整數值,運行時用該字段來標識要回調的方法。 |
_invocationList |
System.Object |
該字段的值一般爲null。當構造委託鏈時它引用一個委託數組。 |
須要注意的一點是,全部的委託都有一個獲取兩個參數的構造方法,這兩個參數分別是對對象的引用以及一個IntPtr類型的用來引用回調函數的句柄(IntPtr 類型被設計成整數,其大小適用於特定平臺。 便是說,此類型的實例在 32 位硬件和操做系統中將是 32 位,在 64 位硬件和操做系統上將是 64 位。IntPtr 對象常可用於保持句柄。 例如,IntPtr 的實例普遍地用在 System.IO.FileStream 類中來保持文件句柄)。代碼以下:
public MyDelegate(object @object, IntPtr method);
可是咱們回去看一看咱們構造委託類型新實例的代碼:
MyDelegate myDelegate = new MyDelegate(myMethod1);
彷佛和構造器的參數對不上呀?那爲什麼編譯器沒有報錯,而是讓這段代碼經過編譯了呢?原來C#的編譯器知道要建立的是委託的實例,於是會分析代碼來肯定引用的是哪一個對象和哪一個方法。分析以後,將對象的引用傳遞給object參數,而方法的引用被傳遞給了method參數。若是myMethod1是靜態方法,那麼object會傳遞爲null。而這個兩個方法實參被傳入構造函數以後,會分別被_target和_methodPtr這兩個私有字段保存,而且_ invocationList字段會被設爲null。
從上面的分析,咱們能夠得出一個結論,即每一個委託對象實際上都是一個包裝了方法和調用該方法時要操做的對象的包裝器。
假設myMethod1是一個MyClass類定義的實例方法。那麼上面那行建立委託實例myDelegate的代碼執行以後,myDelegate內部那三個字段的值以下:
_target |
MyClass的實例 |
_methodPtr |
myMethod1 |
_ invocationList |
null |
假設myMethod1是一個MyClass類定義的靜態方法。那麼上面那行建立委託實例myDelegate的代碼執行以後,myDelegate內部那三個字段的值以下:
_target |
null |
_methodPtr |
myMethod1 |
_ invocationList |
null |
這樣,咱們就瞭解了一個委託實例的建立過程以及其內部結構。那麼接下來咱們繼續探索一下,是如何經過委託實例來調用回調方法的。首先咱們仍是經過一段代碼來開啓咱們的討論。
using UnityEngine; using System.Collections; public class DelegateScript : MonoBehaviour { delegate void MyDelegate(int num); MyDelegate myDelegate; void Start () { myDelegate = new MyDelegate(this.PrintNum); this.Print(10, myDelegate); myDelegate = new MyDelegate(this.PrintDoubleNum); this.Print(10, myDelegate); myDelegate = null; this.Print(10, myDelegate); } void Print(int value, MyDelegate md) { if(md != null) { md(value); } else { Debug.Log("myDelegate is Null!!!"); } } void PrintNum(int num) { Debug.Log ("Print Num: " + num); } void PrintDoubleNum(int num) { int result = num + num; Debug.Log ("result num is : " + result); } }
編譯而且運行以後,輸出的結果以下:
Print Num:10 result num is : 20 myDelegate is Null!!!
咱們能夠注意到,咱們新定義的Print方法將委託實例做爲了其中的一個參數。而且首先檢查傳入的委託實例md是否爲null。那麼這一步是不是畫蛇添足的操做呢?答案是否認的,檢查md是否爲null是必不可少的,這是因爲md僅僅是可能引用了MyDelegate類的實例,但它也有多是null,就像代碼中的第三種狀況所演示的那樣。通過檢查,若是md不是null,則調用回調方法,不過代碼看上去彷佛是調用了一個名爲md,參數爲value的方法:md(value);但事實上並無一個叫作md的方法存在,那麼編譯器是如何來調用正確的回調方法的呢?原來編譯器知道md是引用了委託實例的變量,於是在幕後會生成代碼來調用該委託實例的Invoke方法。換言之,上面剛剛調用回調函數的代碼md(value);被編譯成了以下的形式:
md.Invoke(value);
爲了更深一步的觀察編譯器的行爲,咱們將編譯後的代碼反編譯爲CIL代碼。而且截取其中Print方法部分的CIL代碼:
// method line 4 .method private hidebysig instance default void Print (int32 'value', class DelegateScript/MyDelegate md) cil managed { // Method begins at RVA 0x20c8 // Code size 29 (0x1d) .maxstack 8 IL_0000: ldarg.2 IL_0001: brfalse IL_0012 IL_0006: ldarg.2 IL_0007: ldarg.1 IL_0008: callvirt instance void class DelegateScript/MyDelegate::Invoke(int32) IL_000d: br IL_001c IL_0012: ldstr "myDelegate is Null!!!" IL_0017: call void class [mscorlib]System.Console::WriteLine(string) IL_001c: ret } // end of method DelegateScript::Print
分析這段代碼,咱們能夠發如今IL_0008這行,編譯器爲咱們調用了DelegateScript/MyDelegate::Invoke(int32)方法。那麼咱們是否能夠顯式的調用md的Invoke方法呢?答案是Yes。因此,Print方法徹底能夠改爲以下的定義:
void Print(int value, MyDelegate md) { if(md != null) { md.Invoke(value); } else { Debug.Log("myDelegate is Null!!!"); } }
而一旦調用了委託實例的Invoke方法,那麼以前在構造委託實例時被賦值的字段_target和_methodPtr在此時便派上了用場,它們會爲Invoke方法提供對象和方法信息,使得Invoke可以在指定的對象上調用包裝好的回調方法。OK,本節討論了編譯器如何在幕後爲咱們生成委託類、委託實例的內部結構以及如何利用委託實例的Invoke方法來調用一個回調函數,那麼咱們接下來繼續來討論一下如何使用委託來回調多個方法。
爲了方便,咱們將用委託調用多個方法簡稱爲委託鏈。而委託鏈是委託對象的集合,能夠利用委託鏈來調用集合中的委託所表明的所有方法。爲了使各位可以更加直觀的瞭解委託鏈,下面咱們經過一段代碼來做爲演示:
using UnityEngine; using System; using System.Collections; public class DelegateScript : MonoBehaviour { delegate void MyDelegate(int num); void Start () { //建立3個MyDelegate委託類的實例 MyDelegate myDelegate1 = new MyDelegate(this.PrintNum); MyDelegate myDelegate2 = new MyDelegate(this.PrintDoubleNum); MyDelegate myDelegate3 = new MyDelegate(this.PrintTripleNum); MyDelegate myDelegates = null; //使用Delegate類的靜態方法Combine myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1); myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate2); myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate3); //將myDelegates傳入Print方法 this.Print(10, myDelegates); } void Print(int value, MyDelegate md) { if(md != null) { md(value); } else { Debug.Log("myDelegate is Null!!!"); } } void PrintNum(int num) { Debug.Log ("1 result Num: " + num); } void PrintDoubleNum(int num) { int result = num + num; Debug.Log ("2 result num is : " + result); } void PrintTripleNum(int num) { int result = num + num + num; Debug.Log ("3 result num is : " + result); } }
編譯而且運行以後(將該腳本掛載在某個遊戲物體上,運行Unity3D便可),能夠看到Unity3D的調試窗口打印出了以下內容:
1 result Num: 10 2 result Num: 20 3 result Num: 30
換句話說,一個委託實例myDelegates中調用了三個回調方法PrintNum、PrintDoubleNum以及PrintTripleNum。下面,讓咱們來分析一下這段代碼。咱們首先構造了三個MyDelegate委託類的實例,並分別賦值給myDelegate一、myDelegate二、myDelegate3這三個變量。而以後的myDelegates初始化爲null,即代表了此時沒有要回調的方法,以後咱們要用它來引用委託鏈,或者說是引用一些委託實例的集合,而這些實例中包裝了要被回調的回調方法。那麼應該如何將委託實例加入到委託鏈中呢?不錯,前文提到過基礎類庫中的另外一個委託類Delegate,它有一個公共靜態方法Combine是專門來處理這種需求的,因此接下來咱們就調用了Delegate.Combine方法將委託加入到委託鏈中。
myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1); myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate2); myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate3);
在第一行代碼中,因爲此時myDelegates是null,於是當Delegate.Combine方法發現要合併的是null和一個委託實例myDelegate1時,Delegate.Combine會直接返回myDelegate1的值,於是第一行代碼執行完畢以後,myDelegates如今引用了myDelegate1所引用的委託實例。
當第二次調用Delegate.Combine方法,繼續合併myDelegates和myDelegate2的時候,Delegate.Combine方法檢測到myDelegates已經再也不是null而是引用了一個委託實例,此時Delegate.Combine方法會構建一個不一樣於myDelegates和myDelegate2的新的委託實例。這個新的委託實例天然會對上文經常提起的_target和_methodPtr這兩個私有字段進行初始化,可是此時須要注意的是,以前一直沒有實際值的_invocationList字段此時被初始化爲一個對委託實例數組的引用。該數組的第一個元素即是包裝了第一個委託實例myDelegate1所引用的PrintNum方法的一個委託實例(即myDelegates此時所引用的委託實例),而數組的第二個元素則是包裝了第二個委託實例myDelegate2所引用的PrintDoubleNum方法的委託實例(即myDelegate2所引用的委託實例)。以後,將這個新建立的委託實例的引用賦值給myDelegates變量,此時myDelegates指向了這個包裝了兩個回調方法的新的委託實例。
接下來,咱們第三次調用了Delegate.Combine方法,繼續將委託實例合併到一個委託鏈中。此次編譯器內部發生的事情和上一次大同小異,Delegate.Combine方法檢測到myDelegates已經引用了一個委託實例,一樣地,此次仍然會建立一個新的委託實例,新委託實例中的那兩個私有字段_target和_methodPtr一樣會被初始化,而_invocationList字段此時一樣被初始化爲一個對委託實例數組的引用,只不過此次的元素多了一個包裝了第三個委託實例myDelegate3中所引用的PrintDoubleNum方法的委託實例(即myDelegate3所引用的委託實例)。以後,將這個新建立的委託實例的引用賦值給myDelegates變量,此時myDelegates指向了這個包裝了三個回調方法的新的委託實例。而上一次合併中_invocationList字段所引用的委託實例數組,此時再也不須要,於是能夠被垃圾回收。
當全部的委託實例都合併到一個委託鏈中,而且myDelegates變量引用了該委託鏈以後,咱們將myDelegates變量做爲參數傳入Print方法中,正如前文所述,此時Print方法中的代碼會隱式的調用MyDelegate委託類型的實例的Invoke方法,也就是調用myDelegates變量所引用的委託實例的Invoke方法。此時Invoke方法發現_invocationList字段已經再也不是null而是引用了一個委託實例的數組,所以會執行一個循環來遍歷該數組中的全部元素,並按照順序調用每一個元素(委託實例)中包裝的回調方法。因此,PrintNum方法首先會被調用,緊跟着的是PrintDoubleNum方法,最後則是PrintTripleNum方法。
有合併,對應的天然就有拆解。於是Delegate除了提供了Combine方法用來合併委託實例以外,還提供了Remove方法用來移除委託實例。例如咱們想移除包裝了PrintDoubleNum方法的委託實例,那麼使用Delegate.Remove的代碼以下:
myDelegates = (MyDelegate)Delegate.Remove(myDelegates, new MyDelegate(PrintDoubleNum));
當Delegate.Remove方法被調用時,它會從後向前掃描myDelegates所引用的委託實例中的委託數組,而且對比委託數組中的元素的_target字段和_methodPtr字段的值是否與第二個參數即新建的MyDelegate委託類的實例中的_target字段和_methodPtr字段的值匹配。若是匹配,且刪除該元素以後,委託實例數組中只剩餘一個元素,則直接返回該元素(委託實例);若是刪除該元素以後,委託實例數組中還有多個元素,那麼就會建立一個新的委託實例,這個新建立的委託實例的_invocationList字段會引用一個由刪除了目標元素以後剩餘的元素所組成的委託實例數組,以後返回該委託實例的引用。固然,若是刪除匹配實例以後,委託實例數組變爲空,那麼Remove就會返回null。須要注意的一點是,Remove方法每次僅僅移除一個匹配的委託實例,而不是刪除全部和目標委託實例匹配的委託實例。
固然,若是每次合併委託和刪除委託都要寫Delegate.Combine和Delegate. Remove則未免顯得太過繁瑣,因此爲了方便使用C#語言的開發者,C#編譯器爲委託類型的實例重載了+=和-+操做符來對應Delegate.Combine和Delegate. Remove。具體的例子,咱們能夠看看下面的這段代碼。
using UnityEngine; using System.Collections; public class MulticastScript : MonoBehaviour { delegate void MultiDelegate(); MultiDelegate myMultiDelegate; void Start () { myMultiDelegate += PowerUp; myMultiDelegate += TurnRed; if(myMultiDelegate != null) { myMultiDelegate(); } } void PowerUp() { print ("Orb is powering up!"); } void TurnRed() { renderer.material.color = Color.red; } }
好,我想到此我已經回答了本小節題目中所提出的那個問題:委託是如何調用多個方法的。可是爲了要實現觀察者模式甚至是咱們本身的消息系統,還有一個大人物不得不介紹,那就是和委託關係密切的事件,那麼下一篇博客就讓咱們走進委託和事件的世界中吧。