那些編譯器藏的和U3D給的
0x00 前言
由于工作繁忙所以距離上一篇博客已經(jīng)過去一個多月的時間了,因此決心這個周末無論如何也得寫點東西出來,既是總結(jié)也是分享。那么本文主要的內(nèi)容集中在了委托的使用以及內(nèi)部結(jié)構(gòu)(當(dāng)然還有事件了,但是受制于篇幅故分為兩篇文章)以及結(jié)合一部分Unity3D的設(shè)計思考。當(dāng)然由于時間倉促,文中難免有一些疏漏和不準(zhǔn)確,也歡迎各位指出,共同進步。
0x01 從觀察者模式說起
在設(shè)計模式中,有一種我們常常會用到的設(shè)計模式——觀察者模式。那么這種設(shè)計模式和我們的主題“如何在Unity3D中使用委托”有什么關(guān)系呢?別急,先讓我們來聊一聊什么是觀察者模式。
首先讓我們來看看報紙和雜志的訂閱是怎么一回事:
- 報社的任務(wù)便是出版報紙。
- 向某家報社訂閱他們的報紙,只要他們有新的報紙出版便會向你發(fā)放。也就是說,只要你是他們的訂閱客戶,便可以一直收到新的報紙。
- 如果不再需要這份報紙,則可以取消訂閱。取消之后,報社便不會再送新的報紙過來。
- 報社和訂閱者是兩個不同的主體,只要報社還一直存在著,不同的訂閱者便可以來訂閱或取消訂閱。
如果各位讀者能看明白我上面所說的報紙和雜志是如何訂閱的,那么各位也就了解了觀察者模式到底是怎么一回事。除了名稱不大一樣,在觀察者模式中,報社或者說出版者被稱為“主題”(Subject),而訂閱者則被稱為“觀察者”(Observer)。將上面的報社和訂閱者的關(guān)系移植到觀察者模式中,就變成了如下這樣:主題(Subject)對象管理某些數(shù)據(jù),當(dāng)主題內(nèi)的數(shù)據(jù)改變時,便會通知已經(jīng)訂閱(注冊)的觀察者,而已經(jīng)注冊主題的觀察者此時便會收到主題數(shù)據(jù)改變的通知并更新,而沒有注冊的對象則不會被通知。
當(dāng)我們試圖去勾勒觀察者模式時,可以使用報紙訂閱服務(wù),或者出版者和訂閱者來比擬。而在實際的開發(fā)中,觀察者模式被定義為了如下這樣:
觀察者模式:定義了對象之間的一對多依賴,這樣一來,當(dāng)一個對象改變狀態(tài)時,它的所有依賴者都會收到通知并自動更新。
那么介紹了這么多觀察者模式,是不是也該說一說委托了呢?是的,C#語言通過委托來實現(xiàn)回調(diào)函數(shù)的機制,而回調(diào)函數(shù)是一種很有用的編程機制,可以被廣泛的用在觀察者模式中。
那么Unity3D本身是否有提供這種機制呢?答案也是肯定的,那么和委托又有什么區(qū)別呢?下面就讓我們來聊一聊這個話題。
0x02 向Unity3D中的SendMessage和BroadcastMessage說拜拜
當(dāng)然,不可否認(rèn)Unity3D游戲引擎的出現(xiàn)是游戲開發(fā)者的一大福音。但不得不說的是,Unity3D的游戲腳本的架構(gòu)中是存在一些缺陷的。一個很好的例子就是本節(jié)要說的圍繞SendMessage和BroadcastMessage而構(gòu)建的消息系統(tǒng)。之所以說Unity3D的這套消息系統(tǒng)存在缺陷,主要是由于SendMessage和BroadcastMessage過于依賴反射機制(reflection)來查找消息對應(yīng)的回調(diào)函數(shù)。頻繁的使用反射自然會影響性能,但是性能的損耗還并非最為嚴(yán)重的問題,更加嚴(yán)重的問題是使用這種機制之后代碼的維護成本。為什么說這樣做是一個很糟糕的事情呢?因為使用字符串來標(biāo)識一個方法可能會導(dǎo)致很多隱患的出現(xiàn)。舉一個例子:假如開發(fā)團隊中某個開發(fā)者決定要重構(gòu)某些代碼,很不巧,這部分代碼便是那些可能要被這些消息調(diào)用的方法定義的代碼,那么如果方法被重新命名甚至被刪除,是否會導(dǎo)致很嚴(yán)重的隱患呢?答案是yes。這種隱患的可怕之處并不在于可能引發(fā)的編譯時錯誤,恰恰相反,這種隱患的可怕之處在于編譯器可能都不會報錯來提醒開發(fā)者某些方法已經(jīng)被改名甚至是不存在了,面對一個能夠正常的運行程序而沒有警覺是最可怕的,而什么時候這個隱患會爆發(fā)呢?就是觸發(fā)了特定的消息而找不到對應(yīng)的方法的時候 ,但這時候發(fā)現(xiàn)問題所在往往已經(jīng)太遲了。
另一個潛在的問題是由于使用了反射機制因而Unity3D的這套消息系統(tǒng)也能夠調(diào)用聲明為私有的方法的。但是如果一個私有方法在聲明的類的內(nèi)部沒有被使用,那么正常的想法肯定都認(rèn)為這是一段廢代碼,因為在這個類的外部不可能有人會調(diào)用它。那么對待廢代碼的態(tài)度是什么呢?我想很多開發(fā)者都會選擇消滅這段廢代碼,那么同樣的隱患又會出現(xiàn),可能在編譯時并沒有問題,甚至程序也能正常運行一段時間,但是只要觸發(fā)了特定的消息而沒有對應(yīng)的方法,那便是這種隱患爆發(fā)的時候。因而,是時候向Unity3D中的SendMessage和BroadcastMessage說拜拜了,讓我們選擇C#的委托來實現(xiàn)自己的消息機制吧。
0x03 認(rèn)識回調(diào)函數(shù)機制----委托
在非托管代碼C/C++中也存在類似的回調(diào)機制,但是這些非成員函數(shù)的地址僅僅是一個內(nèi)存地址。而這個地址并不攜帶任何額外的信息,例如函數(shù)的參數(shù)個數(shù)、參數(shù)類型、函數(shù)的返回值類型,因而我們說非托管C/C++代碼的回調(diào)函數(shù)不是類型安全的。而C#中提供的回調(diào)函數(shù)的機制便是委托,一種類型安全的機制。為了直觀的了解委托,我們先來看一段代碼:
- 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);
- }
- }
DelegateScript類還定義了兩個私有方法PrintNum和DoubleNum,它們的分別實現(xiàn)了打印傳入的參數(shù)和打印傳入的參數(shù)的兩倍的功能。在Start方法中,MyDelegate類的實例myDelegate分別引用了這兩個方法,并且分別調(diào)用了這兩個方法。
看到這里,不知道各位讀者是否會產(chǎn)生一些疑問,為什么一個方法能夠像這樣myDelegate = PrintNum; “賦值”給一個委托呢?這便不得不提C#2為委托提供的方法組轉(zhuǎn)換?;厮軨#1的委托機制,也就是十分原始的委托機制中,如果要創(chuàng)建一個委托實例就必須要同時指定委托類型和要調(diào)用的方法(執(zhí)行的操作),因而剛剛的那行代碼就要被改為:
- new MyDelegate(PrintNum);
即便回到C#1的時代,這行創(chuàng)建新的委托實例的代碼看上去似乎并沒有讓開發(fā)者產(chǎn)生什么不好的印象,但是如果是作為較長的一個表達式的一部分時,就會讓人感覺很冗繁了。一個明顯的例子是在啟動一個新的線程時候的表達式:
- Thread th = new Thread(new ThreadStart(Method));
這樣看起來,C#1中的方式似乎并不簡潔。因而C#2為委托引入了方法組轉(zhuǎn)換機制,即支持從方法到兼容的委托類型的隱式轉(zhuǎn)換。就如同我們一開始的例子中做的那樣。
- //使用方法組轉(zhuǎn)換時,隱式轉(zhuǎn)換會將
- //一個方法組轉(zhuǎn)換為具有兼容簽名的
- //任意委托類型
- myDelegate = PrintNum;
- Thread th = new Thread(Method);
而這套機制之所以叫方法組轉(zhuǎn)換,一個重要的原因就是由于重載,可能不止一個方法適用。例如下面這段代碼所演示的那樣:
- 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)
那么根據(jù)方法組轉(zhuǎn)換機制,在向一個MyDelegate或一個MyDelegate2賦值時,都可以使用PrintNum作為方法組(此時有2個PrintNum,因而是“組”),編譯器會選擇合適的重載版本。
當(dāng)然,涉及到委托的還有它的另外一個特點——委托參數(shù)的逆變性和委托返回類型的協(xié)變性。這個特性在很多文章中也有過介紹,但是這里為了使讀者更加加深印象,因而要具體的介紹一下委托的這種特性。
在為委托實例引用方法時,C#允許引用類型的協(xié)變性和逆變性。協(xié)變性是指方法的返回類型可以是從委托的返回類型派生的一個派生類,也就是說協(xié)變性描述的是委托返回類型。逆變性則是指方法獲取的參數(shù)的類型可以是委托的參數(shù)的類型的基類,換言之逆變性描述的是委托的參數(shù)類型。
例如,我們的項目中存在的基礎(chǔ)單位類(BaseUnitClass)、士兵類(SoldierClass)以及英雄類(HeroClass),其中基礎(chǔ)單位類BaseUnitClass作為基類派生出了士兵類SoldierClass和英雄類HeroClass,那么我們可以定義一個委托,就像下面這樣:
- delegate Object TellMeYourName(SoldierClass soldier);
那么我們完全可以通過構(gòu)造一個該委托類型的實例來引用具有以下原型的方法:
- string TellMeYourNameMethod(BaseUnitClass base);
在這個例子中,TellMeYourNameMethod方法的參數(shù)類型是BaseUnitClass,它是TellMeYourName委托的參數(shù)類型SoldierClass的基類,這種參數(shù)的逆變性是允許的;而TellMeYourNameMethod方法的返回值類型為string,是派生自TellMeYourName委托的返回值類型Object的,因而這種返回類型的協(xié)變性也是允許的。但是有一點需要指出的是,協(xié)變性和逆變性僅僅支持引用類型,所以如果是值類型或void則不支持。下面我們接著舉一個例子,如果將TellMeYourNameMethod方法的返回類型改為值類型int,如下:
- int TellMeYourNameMethod(BaseUnitClass base);
這個方法除了返回類型從string(引用類型)變成了int(值類型)之外,什么都沒有被改變,但是如果要將這個方法綁定到剛剛的委托實例上,編譯器會報錯。雖然int型和string型一樣,都派生自O(shè)bject類,但是int型是值類型,因而是不支持協(xié)變性的。這一點,各位讀者在實際的開發(fā)中一定要注意。
好了,到此我們應(yīng)該對委托有了一個初步的直觀印象。在本節(jié)中我?guī)ьI(lǐng)大家直觀的認(rèn)識了委托如何在代碼中使用,以及通過C#2引入的方法組轉(zhuǎn)換機制為委托實例引用合適的方法以及委托的協(xié)變性和逆變性。那么本節(jié)就到此結(jié)束,接下來讓我們更進一步的探索委托。
0x04 委托是如何實現(xiàn)的
讓我們重新定義一個委托并創(chuàng)建它的實例,之后再為該實例綁定一個方法并調(diào)用它:
- internal delegate void MyDelegate(int number);
- MyDelegate myDelegate = new MyDelegate(myMethod1);
- myDelegate = myMethod2;
- myDelegate(10);
事實上編譯器和Mono運行時在幕后做了大量的工作來隱藏委托機制實現(xiàn)的復(fù)雜性。那么本節(jié)就要來揭開委托到底是如何實現(xiàn)的這個謎題。
下面讓我們把目光重新聚焦在剛剛定義委托類型的那行代碼上:
- internal delegate void MyDelegate(int number);
這行對開發(fā)者們來說十分簡單的代碼背后,編譯器為我們做了哪些幕后的工作呢?
讓我們使用Refactor反編譯C#程序,可以看到如下圖的結(jié)果:
可以看到,編譯器實際上為我們定義了一個完整的類MyDelegate:
- internal class MyDelegate : System.MulticastDelegate
- {
- //構(gòu)造器
- [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]
- public MyDelegate(object @object, IntPtr method);
- // Invoke這個方法的原型和源代碼指定的一樣
- [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]
- public virtual void Invoke(int number);
- //以下的兩個方法實現(xiàn)對綁定的回調(diào)函數(shù)的一步回調(diào)
- [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);
- }
- public static Delegate Combine(
- Delegate a,
- Delegate b
- )
- public static Delegate Remove(
- Delegate source,
- Delegate value
- )
再回到我們的MyDelegate委托類,由于委托是類,因而凡是能夠定義類的地方,都可以定義委托,所以委托類既可以在全局范圍中定義,也可以嵌套在一個類型中定義。同樣,委托類也有訪問修飾符,既可以通過指定委托類的訪問修飾符例如:private、internal、public等等來限定訪問權(quán)限。
由于所有的委托類型都繼承于MulticastDelegate類,因而它們也繼承了MulticastDelegate類的字段、屬性以及方法,下面列出三個最重要的非公有字段:
字段 |
類型 |
作用 |
_target |
System.Object |
當(dāng)委托的實例包裝一個靜態(tài)方法時,該字段為null;當(dāng)委托的實例包裝的是一個實例方法時,這個字段引用的是回調(diào)方法要操作的對象。也就是說,這個字段的值是要傳遞給實例方法的隱式參數(shù)this。 |
_methodPtr |
System.IntPtr |
一個內(nèi)部的整數(shù)值,運行時用該字段來標(biāo)識要回調(diào)的方法。 |
_invocationList |
System.Object |
該字段的值通常為null。當(dāng)構(gòu)造委托鏈時它引用一個委托數(shù)組。 |
需要注意的一點是,所有的委托都有一個獲取兩個參數(shù)的構(gòu)造方法,這兩個參數(shù)分別是對對象的引用以及一個IntPtr類型的用來引用回調(diào)函數(shù)的句柄(IntPtr 類型被設(shè)計成整數(shù),其大小適用于特定平臺。 即是說,此類型的實例在 32 位硬件和操作系統(tǒng)中將是 32 位,在 64 位硬件和操作系統(tǒng)上將是 64 位。IntPtr 對象常可用于保持句柄。 例如,IntPtr 的實例廣泛地用在 System.IO.FileStream 類中來保持文件句柄)。代碼如下:
- public MyDelegate(object @object, IntPtr method);
但是我們回去看一看我們構(gòu)造委托類型新實例的代碼:
- MyDelegate myDelegate = new MyDelegate(myMethod1);
似乎和構(gòu)造器的參數(shù)對不上呀?那為何編譯器沒有報錯,而是讓這段代碼通過編譯了呢?原來C#的編譯器知道要創(chuàng)建的是委托的實例,因而會分析代碼來確定引用的是哪個對象和哪個方法。分析之后,將對象的引用傳遞給object參數(shù),而方法的引用被傳遞給了method參數(shù)。如果myMethod1是靜態(tài)方法,那么object會傳遞為null。而這個兩個方法實參被傳入構(gòu)造函數(shù)之后,會分別被_target和_methodPtr這兩個私有字段保存,并且_ invocationList字段會被設(shè)為null。
從上面的分析,我們可以得出一個結(jié)論,即每個委托對象實際上都是一個包裝了方法和調(diào)用該方法時要操作的對象的包裝器。
假設(shè)myMethod1是一個MyClass類定義的實例方法。那么上面那行創(chuàng)建委托實例myDelegate的代碼執(zhí)行之后,myDelegate內(nèi)部那三個字段的值如下:
_target |
MyClass的實例 |
_methodPtr |
myMethod1 |
_ invocationList |
null |
假設(shè)myMethod1是一個MyClass類定義的靜態(tài)方法。那么上面那行創(chuàng)建委托實例myDelegate的代碼執(zhí)行之后,myDelegate內(nèi)部那三個字段的值如下:
_target |
null |
_methodPtr |
myMethod1 |
_ invocationList |
null |
這樣,我們就了解了一個委托實例的創(chuàng)建過程以及其內(nèi)部結(jié)構(gòu)。那么接下來我們繼續(xù)探索一下,是如何通過委托實例來調(diào)用回調(diào)方法的。首先我們還是通過一段代碼來開啟我們的討論。
- 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);
- }
- }
編譯并且運行之后,輸出的結(jié)果如下:
- Print Num:10
- result num is : 20
- myDelegate is Null!!!
#p#
我們可以注意到,我們新定義的Print方法將委托實例作為了其中的一個參數(shù)。并且首先檢查傳入的委托實例md是否為null。那么這一步是否是多此一舉的操作呢?答案是否定的,檢查md是否為null是必不可少的,這是由于md僅僅是可能引用了MyDelegate類的實例,但它也有可能是null,就像代碼中的第三種情況所演示的那樣。經(jīng)過檢查,如果md不是null,則調(diào)用回調(diào)方法,不過代碼看上去似乎是調(diào)用了一個名為md,參數(shù)為value的方法:md(value);但事實上并沒有一個叫做md的方法存在,那么編譯器是如何來調(diào)用正確的回調(diào)方法的呢?原來編譯器知道m(xù)d是引用了委托實例的變量,因而在幕后會生成代碼來調(diào)用該委托實例的Invoke方法。換言之,上面剛剛調(diào)用回調(diào)函數(shù)的代碼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
分析這段代碼,我們可以發(fā)現(xiàn)在IL_0008這行,編譯器為我們調(diào)用了DelegateScript/MyDelegate::Invoke(int32)方法。那么我們是否可以顯式的調(diào)用md的Invoke方法呢?答案是Yes。所以,Print方法完全可以改成如下的定義:
- void Print(int value, MyDelegate md)
- {
- if(md != null)
- {
- md.Invoke(value);
- }
- else
- {
- Debug.Log("myDelegate is Null!!!");
- }
- }
而一旦調(diào)用了委托實例的Invoke方法,那么之前在構(gòu)造委托實例時被賦值的字段_target和_methodPtr在此時便派上了用場,它們會為Invoke方法提供對象和方法信息,使得Invoke能夠在指定的對象上調(diào)用包裝好的回調(diào)方法。OK,本節(jié)討論了編譯器如何在幕后為我們生成委托類、委托實例的內(nèi)部結(jié)構(gòu)以及如何利用委托實例的Invoke方法來調(diào)用一個回調(diào)函數(shù),那么我們接下來繼續(xù)來討論一下如何使用委托來回調(diào)多個方法。
0x05 委托是如何調(diào)用多個方法的?
為了方便,我們將用委托調(diào)用多個方法簡稱為委托鏈。而委托鏈?zhǔn)俏袑ο蟮募?,可以利用委托鏈來調(diào)用集合中的委托所代表的全部方法。為了使各位能夠更加直觀的了解委托鏈,下面我們通過一段代碼來作為演示:
- using UnityEngine;
- using System;
- using System.Collections;
- public class DelegateScript : MonoBehaviour
- {
- delegate void MyDelegate(int num);
- void Start ()
- {
- //創(chuàng)建3個MyDelegate委托類的實例
- MyDelegate myDelegate1 = new MyDelegate(this.PrintNum);
- MyDelegate myDelegate2 = new MyDelegate(this.PrintDoubleNum);
- MyDelegate myDelegate3 = new MyDelegate(this.PrintTripleNum);
- MyDelegate myDelegates = null;
- //使用Delegate類的靜態(tài)方法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的調(diào)試窗口打印出了如下內(nèi)容:
- 1 result Num: 10
- 2 result Num: 20
- 3 result Num: 30
換句話說,一個委托實例myDelegates中調(diào)用了三個回調(diào)方法PrintNum、PrintDoubleNum以及PrintTripleNum。下面,讓我們來分析一下這段代碼。我們首先構(gòu)造了三個MyDelegate委托類的實例,并分別賦值給myDelegate1、myDelegate2、myDelegate3這三個變量。而之后的myDelegates初始化為null,即表明了此時沒有要回調(diào)的方法,之后我們要用它來引用委托鏈,或者說是引用一些委托實例的集合,而這些實例中包裝了要被回調(diào)的回調(diào)方法。那么應(yīng)該如何將委托實例加入到委托鏈中呢?不錯,前文提到過基礎(chǔ)類庫中的另一個委托類Delegate,它有一個公共靜態(tài)方法Combine是專門來處理這種需求的,所以接下來我們就調(diào)用了Delegate.Combine方法將委托加入到委托鏈中。
- myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1);
- myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate2);
- myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate3);
在***行代碼中,由于此時myDelegates是null,因而當(dāng)Delegate.Combine方法發(fā)現(xiàn)要合并的是null和一個委托實例myDelegate1時,Delegate.Combine會直接返回myDelegate1的值,因而***行代碼執(zhí)行完畢之后,myDelegates現(xiàn)在引用了myDelegate1所引用的委托實例。
當(dāng)?shù)诙握{(diào)用Delegate.Combine方法,繼續(xù)合并myDelegates和myDelegate2的時候,Delegate.Combine方法檢測到myDelegates已經(jīng)不再是null而是引用了一個委托實例,此時Delegate.Combine方法會構(gòu)建一個不同于myDelegates和myDelegate2的新的委托實例。這個新的委托實例自然會對上文常常提起的_target和_methodPtr這兩個私有字段進行初始化,但是此時需要注意的是,之前一直沒有實際值的_invocationList字段此時被初始化為一個對委托實例數(shù)組的引用。該數(shù)組的***個元素便是包裝了***個委托實例myDelegate1所引用的PrintNum方法的一個委托實例(即myDelegates此時所引用的委托實例),而數(shù)組的第二個元素則是包裝了第二個委托實例myDelegate2所引用的PrintDoubleNum方法的委托實例(即myDelegate2所引用的委托實例)。之后,將這個新創(chuàng)建的委托實例的引用賦值給myDelegates變量,此時myDelegates指向了這個包裝了兩個回調(diào)方法的新的委托實例。
接下來,我們第三次調(diào)用了Delegate.Combine方法,繼續(xù)將委托實例合并到一個委托鏈中。這次編譯器內(nèi)部發(fā)生的事情和上一次大同小異,Delegate.Combine方法檢測到myDelegates已經(jīng)引用了一個委托實例,同樣地,這次仍然會創(chuàng)建一個新的委托實例,新委托實例中的那兩個私有字段_target和_methodPtr同樣會被初始化,而_invocationList字段此時同樣被初始化為一個對委托實例數(shù)組的引用,只不過這次的元素多了一個包裝了第三個委托實例myDelegate3中所引用的PrintDoubleNum方法的委托實例(即myDelegate3所引用的委托實例)。之后,將這個新創(chuàng)建的委托實例的引用賦值給myDelegates變量,此時myDelegates指向了這個包裝了三個回調(diào)方法的新的委托實例。而上一次合并中_invocationList字段所引用的委托實例數(shù)組,此時不再需要,因而可以被垃圾回收。
當(dāng)所有的委托實例都合并到一個委托鏈中,并且myDelegates變量引用了該委托鏈之后,我們將myDelegates變量作為參數(shù)傳入Print方法中,正如前文所述,此時Print方法中的代碼會隱式的調(diào)用MyDelegate委托類型的實例的Invoke方法,也就是調(diào)用myDelegates變量所引用的委托實例的Invoke方法。此時Invoke方法發(fā)現(xiàn)_invocationList字段已經(jīng)不再是null而是引用了一個委托實例的數(shù)組,因此會執(zhí)行一個循環(huán)來遍歷該數(shù)組中的所有元素,并按照順序調(diào)用每個元素(委托實例)中包裝的回調(diào)方法。所以,PrintNum方法首先會被調(diào)用,緊跟著的是PrintDoubleNum方法,***則是PrintTripleNum方法。
有合并,對應(yīng)的自然就有拆解。因而Delegate除了提供了Combine方法用來合并委托實例之外,還提供了Remove方法用來移除委托實例。例如我們想移除包裝了PrintDoubleNum方法的委托實例,那么使用Delegate.Remove的代碼如下:
- myDelegates = (MyDelegate)Delegate.Remove(myDelegates, new MyDelegate(PrintDoubleNum));
當(dāng)Delegate.Remove方法被調(diào)用時,它會從后向前掃描myDelegates所引用的委托實例中的委托數(shù)組,并且對比委托數(shù)組中的元素的_target字段和_methodPtr字段的值是否與第二個參數(shù)即新建的MyDelegate委托類的實例中的_target字段和_methodPtr字段的值匹配。如果匹配,且刪除該元素之后,委托實例數(shù)組中只剩余一個元素,則直接返回該元素(委托實例);如果刪除該元素之后,委托實例數(shù)組中還有多個元素,那么就會創(chuàng)建一個新的委托實例,這個新創(chuàng)建的委托實例的_invocationList字段會引用一個由刪除了目標(biāo)元素之后剩余的元素所組成的委托實例數(shù)組,之后返回該委托實例的引用。當(dāng)然,如果刪除匹配實例之后,委托實例數(shù)組變?yōu)榭?,那么Remove就會返回null。需要注意的一點是,Remove方法每次僅僅移除一個匹配的委托實例,而不是刪除所有和目標(biāo)委托實例匹配的委托實例。
當(dāng)然,如果每次合并委托和刪除委托都要寫Delegate.Combine和Delegate. Remove則未免顯得太過繁瑣,所以為了方便使用C#語言的開發(fā)者,C#編譯器為委托類型的實例重載了+=和-+操作符來對應(yīng)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;
- }
- }
好,我想到此我已經(jīng)回答了本小節(jié)題目中所提出的那個問題:委托是如何調(diào)用多個方法的。但是為了要實現(xiàn)觀察者模式甚至是我們自己的消息系統(tǒng),還有一個大人物不得不介紹,那就是和委托關(guān)系密切的事件,那么下一篇博客就讓我們走進委托和事件的世界中吧。