從頭到尾看委託

    目錄

  1. 委託介紹
  2. 何處定義委託?
  3. 委託既可以封裝靜態方法,也可以封裝實例方法,還可以封裝匿名方法
  4. 處理髮布、訂閱關係的幾種方式
  5. 委託本質
  6. 爲什麼即有Delegate類,又有MulticastDelegate類,這兩個類有什麼區別?
  7. 委託判等
  8. 獲取委託鏈中各個委託的返回值

    安全性
    委託相對於其它語言的回調函數,最大的好處在與其的安全性。
    委託是回調函數的安全類型包裝(相對於非託管程序)。C++編寫的非託管程序進行回調時很容易出錯(C中的函數指針只不過是一個指向存儲單元的指針,我們無法說出這個指針實際指向什麼,像參數和返回類型等就更無從知曉了)。由於委託的存在,託管應用程序不會出現這樣的情況。委託通常用來定義響應事件的回調方法的簽名。
    C#中的委託類似於C或C++中的函數指針。使用委託使程序員可以將方法引用封裝在委託對象內( 所以這裏的“引用”不是原始內存地址,而是包裝了方法的內存地址的委託實例 )。然後可以將給委託對象傳遞可調用所引用方法的代碼,而不必在編譯時知道將調用哪個方法。與C或C++中的函數指針不同,委託是面向對象、類型安全的,並且是安全的。
    委託聲明定義一種類型,它用一組特定的參數以及返回類型封裝方法。
    對於靜態方法,委託對象封裝要調用的方法。
    對於實例方法,委託對象同時封裝一個實例和該實例上的一個方法。
    如果你有一個委託對象和一組適當的參數,則可以用這些參數調用該委託。
    委託的一個有趣且有用的屬性是: 它不知道或不關心自己引用的對象的類,任何對象都可以,只是方法的參數類型必須與委託的參數類型和返回類型相匹配。   
    另外一點,委託實現了發佈委託的類與訂閱委託的類之間實現瞭解耦。發佈委託的類只管向外面其它類公佈,我這裏有這樣一個“接口”,具體運行時調用時會執行成什麼樣子,發佈委託類不管;訂閱委託的類只管實現如何做就可以了,具體怎麼調用到它的,訂閱委託類不用管。  

    定義委託實際上是定義一個繼承自System.MultiCastDelegate的一個類,所以定義類的地方就可以定義委託。
    個人認爲,如果定義了一個委託,不止一個類要發佈該委託,那麼該委託定義就應該放在類的外面進行定義;如果定義的委託就是給某個發佈者使用的,那麼直接定義在這個發佈者類裏面就可以了。

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. namespace DelegatePractise
  6. {
  7.     #region 定義委託
  8.     public delegate void ShowInfoHandler(string info);
  9.     #endregion
  10.     #region 委託發佈者
  11.     class Distributor 
  12.     {
  13.         public ShowInfoHandler handler;
  14.         public void show( string something) 
  15.         {
  16.             if ( this.handler != null )
  17.              {
  18.                  handler(something);
  19.              }
  20.         }
  21.     }
  22.     #endregion
  23.     #region 委託訂閱者
  24.     class Subscriber 
  25.     {
  26.         //實現訂閱的實例方法
  27.         public void InstanceMethod(string something) 
  28.         {
  29.             Console.WriteLine("i am in instance method!" + something);
  30.         }
  31.         //實現訂閱的靜態方法
  32.         public static void SaySomething(string something) 
  33.         {
  34.             Console.WriteLine( "i am in static method!" + something );
  35.         }
  36.     }
  37.     #endregion
  38.     class Test
  39.     {
  40.         static void Main(string[] args)
  41.         {
  42.             Distributor distributor = new Distributor();
  43.             Subscriber subscriber = new Subscriber();
  44.             //委託封裝實例方法
  45.             distributor.handler += subscriber.InstanceMethod;
  46.             //委託封裝靜態方法
  47.             distributor.handler += Subscriber.SaySomething;
  48.             //委託封裝匿名方法
  49.             distributor.handler += delegate(string noName)
  50.             {
  51.                 Console.WriteLine("i am in a noname method!" + noName);
  52.             };
  53.             distributor.show("haha");
  54.             Console.ReadLine();
  55.         }
  56.     }
  57. }

 

    第一種方式:由第三方類來關聯發佈者和訂閱者
    在這種方式下,發佈者只管發佈,訂閱者只管實現,關聯關係在第三方類中進行。代碼示例如下:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. namespace DelegatePractise
  6. {
  7.     #region 定義委託
  8.     public delegate void ShowInfoHandler(string info);
  9.     #endregion
  10.     #region 委託發佈者
  11.     class Distributor 
  12.     {
  13.         //要發佈出去的東東
  14.         public ShowInfoHandler showInfoDel;
  15.         public void show(string something) 
  16.         {
  17.              if ( showInfoDel != null )
  18.              {
  19.                  showInfoDel( something );
  20.              }
  21.         }
  22.     }
  23.     #endregion
  24.     #region 委託訂閱者
  25.     class Subscriber 
  26.     {
  27.         //實現訂閱的方法
  28.         public void SaySomething(string something) 
  29.         {
  30.             Console.WriteLine( something );
  31.         }
  32.     }
  33.     #endregion
  34.     class Test
  35.     {
  36.         static void Main(string[] args)
  37.         {
  38.             Distributor distributor = new Distributor();
  39.             Subscriber subscriber = new Subscriber();
  40.             //關聯發佈者和訂閱者之間的訂閱關係
  41.             distributor.showInfoDel += subscriber.SaySomething;
  42.             distributor.show("我是要顯示的東東!");
  43.             Console.ReadLine();
  44.         }
  45.     }
  46. }

    從上面的例子中可看出,發佈者和訂閱者的關聯關係是在第三方類Test中關聯起來的。
    如果訂閱者比較少,可以使用這種方式,但是,如果訂閱者很多、並且沒什麼規律的話,這樣來關聯發佈者和訂閱者就很累了,讓訂閱者自己內部來進行訂閱,就輕鬆一些。 
    第二種方式:訂閱者使用方法來訂閱發佈者發佈的委託 
    在這種方式下,訂閱者使用方法訂閱發佈者發佈的委託,不用第三方類來進行關聯發佈者和訂閱者。代碼如下:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. namespace DelegatePractise
  6. {
  7.     #region 定義委託
  8.     public delegate void ShowInfoHandler(string info);
  9.     #endregion
  10.     #region 委託發佈者
  11.     class Distributor 
  12.     {
  13.         //要發佈出去的東東
  14.         public ShowInfoHandler showInfoDel;
  15.         public void show(string something) 
  16.         {
  17.              if ( showInfoDel != null )
  18.              {
  19.                  showInfoDel( something );
  20.              }
  21.         }
  22.     }
  23.     #endregion
  24.     #region 委託訂閱者
  25.     class Subscriber 
  26.     {
  27.         //實現訂閱的方法
  28.         public void SaySomething(string something) 
  29.         {
  30.             Console.WriteLine( something );
  31.         }
  32.         //訂閱者使用方法來訂閱發佈者的委託
  33.         public void SubscribeDel(Distributor dis) 
  34.         {
  35.             dis.showInfoDel += SaySomething;
  36.         }
  37.     }
  38.     #endregion
  39.     class Test
  40.     {
  41.         static void Main(string[] args)
  42.         {
  43.             Distributor distributor = new Distributor();
  44.             Subscriber subscriber = new Subscriber();
  45.             //訂閱者訂閱發佈者的委託
  46.             subscriber.SubscribeDel(distributor);
  47.             distributor.show("我是要顯示的東東!");
  48.             Console.ReadLine();
  49.         }
  50.     }
  51. }

    第三種方式:使用static readonly字段

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. namespace DelegatePractise
  6. {
  7.     #region 定義委託
  8.     public delegate void ShowInfoHandler(string info);
  9.     #endregion
  10.     #region 委託發佈者
  11.     class Distributor 
  12.     {
  13.         public void show( ShowInfoHandler handler,string something) 
  14.         {
  15.              if ( handler != null)
  16.              {
  17.                  handler(something);
  18.              }
  19.         }
  20.     }
  21.     #endregion
  22.     #region 委託訂閱者
  23.     class Subscriber 
  24.     {
  25.         //在類裏面已經使用static readonly的字段的方式把委託和方法關聯好了
  26.         //注意使用這種方式時,只能使用靜態的方法和靜態的字段
  27.         public static readonly ShowInfoHandler handler = new ShowInfoHandler( SaySomething );
  28.         //實現訂閱的方法
  29.         public static void SaySomething(string something) 
  30.         {
  31.             Console.WriteLine( something );
  32.         }
  33.     }
  34.     #endregion
  35.     class Test
  36.     {
  37.         static void Main(string[] args)
  38.         {
  39.             Distributor distributor = new Distributor();
  40.             distributor.show(Subscriber.handler, "hello");
  41.             Console.ReadLine();
  42.         }
  43.     }
  44. }

    實際上,在上面的例子中,發佈者並沒有定義一個委託字段來發佈一個委託,但它使用了一個帶委託類型參數的方法,故我們還把它當做發佈者。這種方式下,發佈者只管使用委託,具體委託關聯了哪些方法,它不管;委託具體如何關聯,以及被調用後做什麼,全部都在訂閱者中進行實現,很好的實現了類與類之間的解耦。
    第四種方式:訂閱者使用屬性代替static readonly委託字段
    使用static readonly字段方式的問題在於,不管你有沒有使用到這個委託,你都必須實例化。故可以優化爲使用屬性方式,使用到該屬性時,才返回一個委託實例。具體代碼如下:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. namespace DelegatePractise
  6. {
  7.     #region 定義委託
  8.     public delegate void ShowInfoHandler(string info);
  9.     #endregion
  10.     #region 委託發佈者
  11.     class Distributor 
  12.     {
  13.         public void show( ShowInfoHandler handler,string something) 
  14.         {
  15.              if ( handler != null)
  16.              {
  17.                  handler(something);
  18.              }
  19.         }
  20.     }
  21.     #endregion
  22.     #region 委託訂閱者
  23.     class Subscriber 
  24.     {
  25.         public static ShowInfoHandler Handler 
  26.         {
  27.             get 
  28.             {
  29.                 return new ShowInfoHandler( SaySomething );
  30.             }
  31.         }
  32.         //實現訂閱的方法
  33.         public static void SaySomething(string something) 
  34.         {
  35.             Console.WriteLine( something );
  36.         }
  37.     }
  38.     #endregion
  39.     class Test
  40.     {
  41.         static void Main(string[] args)
  42.         {
  43.             Distributor distributor = new Distributor();
  44.             distributor.show(Subscriber.Handler, "hello");
  45.             Console.ReadLine();
  46.         }
  47.     }
  48. }

    我們先來把第一種發佈委託方式的代碼反編譯,來看看能發現什麼,IL代碼如下:

  1. .namespace DelegatePractise
  2. {
  3.     .class private auto ansi beforefieldinit Distributor
  4.         extends [mscorlib]System.Object
  5.     {
  6.         .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
  7.         {
  8.         }
  9.         .method public hidebysig instance void show(string something) cil managed
  10.         {
  11.         }
  12.         .field public class DelegatePractise.ShowInfoHandler showInfoDel
  13.     }
  14.     .class public auto ansi sealed ShowInfoHandler
  15.         extends [mscorlib]System.MulticastDelegate
  16.     {
  17.         .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed
  18.         {
  19.         }
  20.         .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(string info, class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed
  21.         {
  22.         }
  23.         .method public hidebysig newslot virtual instance void EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
  24.         {
  25.         }
  26.         .method public hidebysig newslot virtual instance void Invoke(string info) runtime managed
  27.         {
  28.         }
  29.     }
  30.     .class private auto ansi beforefieldinit Subscriber
  31.         extends [mscorlib]System.Object
  32.     {
  33.         .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
  34.         {
  35.         }
  36.         .method public hidebysig instance void SaySomething(string something) cil managed
  37.         {
  38.         }
  39.     }
  40.     .class private auto ansi beforefieldinit Test
  41.         extends [mscorlib]System.Object
  42.     {
  43.         .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
  44.         {
  45.         }
  46.         .method private hidebysig static void Main(string[] args) cil managed
  47.         {
  48.             .entrypoint
  49.         }
  50.     }
  51. }

    從上面的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()方法。

      

發佈了29 篇原創文章 · 獲贊 1 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章