程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 匹夫細說C#:庖丁解牛聊委托,那些編譯器藏的和U3D給的,

匹夫細說C#:庖丁解牛聊委托,那些編譯器藏的和U3D給的,

編輯:C#入門知識

匹夫細說C#:庖丁解牛聊委托,那些編譯器藏的和U3D給的,


0x00 前言

由於工作繁忙所以距離上一篇博客已經過去一個多月的時間了,因此決心這個周末無論如何也得寫點東西出來,既是總結也是分享。那麼本文主要的內容集中在了委托的使用以及內部結構(當然還有事件了,但是受制於篇幅故分為兩篇文章)以及結合一部分Unity3D的設計思考。當然由於時間倉促,文中難免有一些疏漏和不准確,也歡迎各位指出,共同進步。

0x01 從觀察者模式說起

在設計模式中,有一種我們常常會用到的設計模式——觀察者模式。那麼這種設計模式和我們的主題“如何在Unity3D中使用委托”有什麼關系呢?別急,先讓我們來聊一聊什麼是觀察者模式。

首先讓我們來看看報紙和雜志的訂閱是怎麼一回事:

如果各位讀者能看明白我上面所說的報紙和雜志是如何訂閱的,那麼各位也就了解了觀察者模式到底是怎麼一回事。除了名稱不大一樣,在觀察者模式中,報社或者說出版者被稱為“主題”(Subject),而訂閱者則被稱為“觀察者”(Observer)。將上面的報社和訂閱者的關系移植到觀察者模式中,就變成了如下這樣:主題(Subject)對象管理某些數據,當主題內的數據改變時,便會通知已經訂閱(注冊)的觀察者,而已經注冊主題的觀察者此時便會收到主題數據改變的通知並更新,而沒有注冊的對象則不會被通知。

當我們試圖去勾勒觀察者模式時,可以使用報紙訂閱服務,或者出版者和訂閱者來比擬。而在實際的開發中,觀察者模式被定義為了如下這樣:

觀察者模式:定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的所有依賴者都會收到通知並自動更新。

那麼介紹了這麼多觀察者模式,是不是也該說一說委托了呢?是的,C#語言通過委托來實現回調函數的機制,而回調函數是一種很有用的編程機制,可以被廣泛的用在觀察者模式中。

那麼Unity3D本身是否有提供這種機制呢?答案也是肯定的,那麼和委托又有什麼區別呢?下面就讓我們來聊一聊這個話題。

0x02 向Unity3D中的SendMessage和BroadcastMessage說拜拜

當然,不可否認Unity3D游戲引擎的出現是游戲開發者的一大福音。但不得不說的是,Unity3D的游戲腳本的架構中是存在一些缺陷的。一個很好的例子就是本節要說的圍繞SendMessage和BroadcastMessage而構建的消息系統。之所以說Unity3D的這套消息系統存在缺陷,主要是由於SendMessage和BroadcastMessage過於依賴反射機制(reflection)來查找消息對應的回調函數。頻繁的使用反射自然會影響性能,但是性能的損耗還並非最為嚴重的問題,更加嚴重的問題是使用這種機制之後代碼的維護成本。為什麼說這樣做是一個很糟糕的事情呢?因為使用字符串來標識一個方法可能會導致很多隱患的出現。舉一個例子:假如開發團隊中某個開發者決定要重構某些代碼,很不巧,這部分代碼便是那些可能要被這些消息調用的方法定義的代碼,那麼如果方法被重新命名甚至被刪除,是否會導致很嚴重的隱患呢?答案是yes。這種隱患的可怕之處並不在於可能引發的編譯時錯誤,恰恰相反,這種隱患的可怕之處在於編譯器可能都不會報錯來提醒開發者某些方法已經被改名甚至是不存在了,面對一個能夠正常的運行程序而沒有警覺是最可怕的,而什麼時候這個隱患會爆發呢?就是觸發了特定的消息而找不到對應的方法的時候 ,但這時候發現問題所在往往已經太遲了。

另一個潛在的問題是由於使用了反射機制因而Unity3D的這套消息系統也能夠調用聲明為私有的方法的。但是如果一個私有方法在聲明的類的內部沒有被使用,那麼正常的想法肯定都認為這是一段廢代碼,因為在這個類的外部不可能有人會調用它。那麼對待廢代碼的態度是什麼呢?我想很多開發者都會選擇消滅這段廢代碼,那麼同樣的隱患又會出現,可能在編譯時並沒有問題,甚至程序也能正常運行一段時間,但是只要觸發了特定的消息而沒有對應的方法,那便是這種隱患爆發的時候。因而,是時候向Unity3D中的SendMessage和BroadcastMessage說拜拜了,讓我們選擇C#的委托來實現自己的消息機制吧。

0x03 認識回調函數機制----委托

在非托管代碼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引入的方法組轉換機制為委托實例引用合適的方法以及委托的協變性和逆變性。那麼本節就到此結束,接下來讓我們更進一步的探索委托。

0x04 委托是如何實現的

讓我們重新定義一個委托並創建它的實例,之後再為該實例綁定一個方法並調用它:

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方法來調用一個回調函數,那麼我們接下來繼續來討論一下如何使用委托來回調多個方法。

0x05 委托是如何調用多個方法的?

為了方便,我們將用委托調用多個方法簡稱為委托鏈。而委托鏈是委托對象的集合,可以利用委托鏈來調用集合中的委托所代表的全部方法。為了使各位能夠更加直觀的了解委托鏈,下面我們通過一段代碼來作為演示:

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委托類的實例,並分別賦值給myDelegate1、myDelegate2、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;

    }

}

好,我想到此我已經回答了本小節題目中所提出的那個問題:委托是如何調用多個方法的。但是為了要實現觀察者模式甚至是我們自己的消息系統,還有一個大人物不得不介紹,那就是和委托關系密切的事件,那麼下一篇博客就讓我們走進委托和事件的世界中吧。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved