- 委託介紹
- 何處定義委託?
- 委託既可以封裝靜態方法,也可以封裝實例方法,還可以封裝匿名方法
- 處理髮布、訂閱關係的幾種方式
- 委託本質
- 爲什麼即有Delegate類,又有MulticastDelegate類,這兩個類有什麼區別?
- 委託判等
- 獲取委託鏈中各個委託的返回值
安全性:
委託相對於其它語言的回調函數,最大的好處在與其的安全性。
委託是回調函數的安全類型包裝(相對於非託管程序)。C++編寫的非託管程序進行回調時很容易出錯(C中的函數指針只不過是一個指向存儲單元的指針,我們無法說出這個指針實際指向什麼,像參數和返回類型等就更無從知曉了)。由於委託的存在,託管應用程序不會出現這樣的情況。委託通常用來定義響應事件的回調方法的簽名。
C#中的委託類似於C或C++中的函數指針。使用委託使程序員可以將方法引用封裝在委託對象內( 所以這裏的“引用”不是原始內存地址,而是包裝了方法的內存地址的委託實例 )。然後可以將給委託對象傳遞可調用所引用方法的代碼,而不必在編譯時知道將調用哪個方法。與C或C++中的函數指針不同,委託是面向對象、類型安全的,並且是安全的。
委託聲明定義一種類型,它用一組特定的參數以及返回類型封裝方法。
對於靜態方法,委託對象封裝要調用的方法。
對於實例方法,委託對象同時封裝一個實例和該實例上的一個方法。
如果你有一個委託對象和一組適當的參數,則可以用這些參數調用該委託。
委託的一個有趣且有用的屬性是: 它不知道或不關心自己引用的對象的類,任何對象都可以,只是方法的參數類型必須與委託的參數類型和返回類型相匹配。
另外一點,委託實現了發佈委託的類與訂閱委託的類之間實現瞭解耦。發佈委託的類只管向外面其它類公佈,我這裏有這樣一個“接口”,具體運行時調用時會執行成什麼樣子,發佈委託類不管;訂閱委託的類只管實現如何做就可以了,具體怎麼調用到它的,訂閱委託類不用管。
定義委託實際上是定義一個繼承自System.MultiCastDelegate的一個類,所以定義類的地方就可以定義委託。
個人認爲,如果定義了一個委託,不止一個類要發佈該委託,那麼該委託定義就應該放在類的外面進行定義;如果定義的委託就是給某個發佈者使用的,那麼直接定義在這個發佈者類裏面就可以了。
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定義委託
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委託發佈者
- class Distributor
- {
- public ShowInfoHandler handler;
- public void show( string something)
- {
- if ( this.handler != null )
- {
- handler(something);
- }
- }
- }
- #endregion
- #region 委託訂閱者
- class Subscriber
- {
- //實現訂閱的實例方法
- public void InstanceMethod(string something)
- {
- Console.WriteLine("i am in instance method!" + something);
- }
- //實現訂閱的靜態方法
- public static void SaySomething(string something)
- {
- Console.WriteLine( "i am in static method!" + something );
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- Subscriber subscriber = new Subscriber();
- //委託封裝實例方法
- distributor.handler += subscriber.InstanceMethod;
- //委託封裝靜態方法
- distributor.handler += Subscriber.SaySomething;
- //委託封裝匿名方法
- distributor.handler += delegate(string noName)
- {
- Console.WriteLine("i am in a noname method!" + noName);
- };
- distributor.show("haha");
- Console.ReadLine();
- }
- }
- }
第一種方式:由第三方類來關聯發佈者和訂閱者
在這種方式下,發佈者只管發佈,訂閱者只管實現,關聯關係在第三方類中進行。代碼示例如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定義委託
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委託發佈者
- class Distributor
- {
- //要發佈出去的東東
- public ShowInfoHandler showInfoDel;
- public void show(string something)
- {
- if ( showInfoDel != null )
- {
- showInfoDel( something );
- }
- }
- }
- #endregion
- #region 委託訂閱者
- class Subscriber
- {
- //實現訂閱的方法
- public void SaySomething(string something)
- {
- Console.WriteLine( something );
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- Subscriber subscriber = new Subscriber();
- //關聯發佈者和訂閱者之間的訂閱關係
- distributor.showInfoDel += subscriber.SaySomething;
- distributor.show("我是要顯示的東東!");
- Console.ReadLine();
- }
- }
- }
從上面的例子中可看出,發佈者和訂閱者的關聯關係是在第三方類Test中關聯起來的。
如果訂閱者比較少,可以使用這種方式,但是,如果訂閱者很多、並且沒什麼規律的話,這樣來關聯發佈者和訂閱者就很累了,讓訂閱者自己內部來進行訂閱,就輕鬆一些。
第二種方式:訂閱者使用方法來訂閱發佈者發佈的委託
在這種方式下,訂閱者使用方法訂閱發佈者發佈的委託,不用第三方類來進行關聯發佈者和訂閱者。代碼如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定義委託
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委託發佈者
- class Distributor
- {
- //要發佈出去的東東
- public ShowInfoHandler showInfoDel;
- public void show(string something)
- {
- if ( showInfoDel != null )
- {
- showInfoDel( something );
- }
- }
- }
- #endregion
- #region 委託訂閱者
- class Subscriber
- {
- //實現訂閱的方法
- public void SaySomething(string something)
- {
- Console.WriteLine( something );
- }
- //訂閱者使用方法來訂閱發佈者的委託
- public void SubscribeDel(Distributor dis)
- {
- dis.showInfoDel += SaySomething;
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- Subscriber subscriber = new Subscriber();
- //訂閱者訂閱發佈者的委託
- subscriber.SubscribeDel(distributor);
- distributor.show("我是要顯示的東東!");
- Console.ReadLine();
- }
- }
- }
第三種方式:使用static readonly字段
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定義委託
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委託發佈者
- class Distributor
- {
- public void show( ShowInfoHandler handler,string something)
- {
- if ( handler != null)
- {
- handler(something);
- }
- }
- }
- #endregion
- #region 委託訂閱者
- class Subscriber
- {
- //在類裏面已經使用static readonly的字段的方式把委託和方法關聯好了
- //注意使用這種方式時,只能使用靜態的方法和靜態的字段
- public static readonly ShowInfoHandler handler = new ShowInfoHandler( SaySomething );
- //實現訂閱的方法
- public static void SaySomething(string something)
- {
- Console.WriteLine( something );
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- distributor.show(Subscriber.handler, "hello");
- Console.ReadLine();
- }
- }
- }
實際上,在上面的例子中,發佈者並沒有定義一個委託字段來發佈一個委託,但它使用了一個帶委託類型參數的方法,故我們還把它當做發佈者。這種方式下,發佈者只管使用委託,具體委託關聯了哪些方法,它不管;委託具體如何關聯,以及被調用後做什麼,全部都在訂閱者中進行實現,很好的實現了類與類之間的解耦。
第四種方式:訂閱者使用屬性代替static readonly委託字段
使用static readonly字段方式的問題在於,不管你有沒有使用到這個委託,你都必須實例化。故可以優化爲使用屬性方式,使用到該屬性時,才返回一個委託實例。具體代碼如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定義委託
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委託發佈者
- class Distributor
- {
- public void show( ShowInfoHandler handler,string something)
- {
- if ( handler != null)
- {
- handler(something);
- }
- }
- }
- #endregion
- #region 委託訂閱者
- class Subscriber
- {
- public static ShowInfoHandler Handler
- {
- get
- {
- return new ShowInfoHandler( SaySomething );
- }
- }
- //實現訂閱的方法
- public static void SaySomething(string something)
- {
- Console.WriteLine( something );
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- distributor.show(Subscriber.Handler, "hello");
- Console.ReadLine();
- }
- }
- }
我們先來把第一種發佈委託方式的代碼反編譯,來看看能發現什麼,IL代碼如下:
- .namespace DelegatePractise
- {
- .class private auto ansi beforefieldinit Distributor
- extends [mscorlib]System.Object
- {
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- }
- .method public hidebysig instance void show(string something) cil managed
- {
- }
- .field public class DelegatePractise.ShowInfoHandler showInfoDel
- }
- .class public auto ansi sealed ShowInfoHandler
- extends [mscorlib]System.MulticastDelegate
- {
- .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed
- {
- }
- .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(string info, class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed
- {
- }
- .method public hidebysig newslot virtual instance void EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
- {
- }
- .method public hidebysig newslot virtual instance void Invoke(string info) runtime managed
- {
- }
- }
- .class private auto ansi beforefieldinit Subscriber
- extends [mscorlib]System.Object
- {
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- }
- .method public hidebysig instance void SaySomething(string something) cil managed
- {
- }
- }
- .class private auto ansi beforefieldinit Test
- extends [mscorlib]System.Object
- {
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- }
- .method private hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
- }
- }
- }
從上面的IL代碼中我們會發現比我們原先寫的代碼中多了一個類:ShowInfoHandler,該類正是我們定義的委託的名字,這說明定義一個委託,實際上就是定義一個類,只是在c#語言中允許我們像那樣來寫罷了,到了底層,還是一個類。我們能夠從生成的類中發現,該類有四個方法,分別爲構造函數、同步invoke方法、異步invoke方法、異步結束方法。
在構造函數中有兩個參數,第一個參數爲委託的target,如果關聯的方法爲靜態方法,則此object爲null,第二個參數表示關聯方法對應內存中的int入口地址;
Invoke方法的參數跟我們定義委託時的方法簽名密切相關,那個方法簽名的參數是什麼樣子,這裏Invoke方法的參數就是什麼樣子;
BeginInvoke方法的參數也是跟定義委託時的方法簽名密切相關,那個方法簽名的參數是什麼樣子,BeginInvoke方法的前幾個參數就是什麼樣子,它的倒數第二個參數也是個異步委託的實例,表示該異步方法結束時要調用的回調函數,最後一個參數一般就是把當前調用BeginInvoke方法的委託自己傳進去。
EndInvoke方法有一個參數,必須是IAsyncResult類型的,該參數含有異步調用的結果信息。另外注意,IAsyncResult類型的這個參數是必須的,委託方法簽名中的in/out參數也要帶進來,返回值類型則就是委託簽名中的返回值類型。
另外,注意看的話,會發現委託生成的這個類是繼承自System.MulticastDelegate類的, 而且生成的委託類的每個方法都標識爲runtime managed,而不是cil managed,表示這個類裏面究竟要幹什麼,在編譯時是不確定的,只有到了運行時才能夠得知,所以方法體內都是空的。
下面,我們來看看MulticastDelegate類中幾個重要的私有字段:
1)_target,類型爲System.Object,指向委託被調用時應該操作的對象。該字段用於實例方法的回調,如果委託關聯的方法是靜態方法,則此字段爲null。如果跟反射關聯起來,該字段以及_methodPtr這兩個字段非常有用,Delegate類中有相應的屬性來訪問相應的字段。具體委託與事件的關聯應用,後面會有描述。
2)_methodPtr,類型爲System.Int32,標識關聯方法在內存中被調用時的入口地址。
3)_prev,類型爲System.MulticastDelegate,表示在委託鏈中上一個委託,如果本委託就是委託鏈的頭,則此字段就是null。該字段在委託判等時非常必要。
微軟剛開始設計c#語言的時候,Delegate類是用在單播模式下,MulticastDelegate類是用在多播模式下,在編譯的時候,編譯器如果發現該委託簽名返回值是null,則編譯器認爲它是多播模式,自動生成類並繼承自MulticastDelegate,如果該委託方法簽名返回值類型不是null,則編譯器認爲它是單播模式,自動生成類並繼承自Delegate。但是在後面的很長一段時間裏,接近c#快要發佈的時候,發現很多時候雖然實現委託的方法有返回值,但調用委託的時候,這些返回值並不重要,即想讓有返回值的委託類型也繼承自MulticastDelegate,這下可麻煩了,如果是在設計的早期發現這個問題,那麼很容易就溝通解決掉了,但是,在快要發佈的這個時候,如果重新去設計,就要牽涉CLR小組,C#語言小組,編譯器小組等,微軟想想,靠,爲了這麼點個小功能,讓我傷筋動骨,本來不穩定的名聲就在外了,這次如果改一下,如果測試又不徹底,nnd就給人留下把柄了,不划算!不划算歸不划算,問題還是要處理,好在微軟還可以修改自己的C#編譯器,這樣修改了編譯器後,只要是委託,就都繼承自MulticastDelegate了,MulticastDelegate自己呢就繼承自Delegate,Delegate則繼承自Object,這就是委託的繼承體系,Delegete類目前呢,裏面是還有好幾個非常有用的靜態方法的,後面的章節會介紹到。
上面已經說過,Delegate類繼承自Object類。但是Delegate類重寫了Object類的Equals()方法,比較的是委託的_target字段和_method字段是否一致,如果兩個都一致,那麼就返回true,如果有一個不一致,就返回false。但實際上我們的所有委託實際上都是繼承自MulticastDelegate的,而MulticastDelegate又重寫了Delegate的Equals()方法,它先判斷兩個委託的_target和_method以及_prev是否一致,如果不一致,則返回false,如果一致,則接着判斷,兩個委託鏈是否一樣長,不一樣長,也返回false,如果一樣長,則接着判斷各自_prev指向的委託(實際上也是委託鏈)是否相等,這樣判斷,一直到委託鏈的頭。
直接上代碼,關鍵是使用GetInvocationList()方法。