.NET中的委派(Delegates)(轉)

介紹.NET中的委派(Delegates)
----微軟 .NET平臺系列文章之四
譯文/趙湘寧

 

回調函數
    回調函數的確是至今爲止最有用的編程機制之一。C運行時的qsort函數利用回調函數對數組元素進行排序。在Windows中,回調函數更是窗口過程,鉤子過程,異步過程調用,以及目前Microsoft .NET框架所必需的,在整個回調過程中自始至終地使用回調方法。人們可以註冊回調方法以獲得加載/卸載通知,未處理異常通知,數據庫/窗口狀態修改通知,文件系統修改通知,菜單項選擇,完成的異步操作通知,過濾一組條目等等。
   在C/C++中,一個函數的地址就是內存地址。這個地址不會附帶任何其它賦加信息,如函數的參數個數,參數類型,函數的返回值類型以及這個函數的調用規範。簡言之,C/C++回調函數不是類型安全的。
    在.NET框架中,回調函數所受到的重用與它在Windows非受控編程中一樣。不同的是在.NET框架中提供了一種叫委派(delegates)的類型安全機制。我們先來研究一下委派的聲明。下面的代碼展示瞭如何聲明,創建和使用委派:
//
using System;
using System.WinForms;	// 在beta2版本中爲:System.Windows.Forms
using System.IO;

class Set {
   private Object[] items;

   public Set(Int32 numItems) {
      items = new Object[numItems];
      for (Int32 i = 0; i < numItems; i++)
         items[i] = i;
   }

   // 定義 Feedback,類型爲delegate
   // (注意: 這個類型在Set類中是嵌套的)
   public delegate void Feedback(
      Object value, Int32 item, Int32 numItems);

   public void ProcessItems(Feedback feedback) {
      for (Int32 item = 0; item < items.Length; item++) {
         if (feedback != null) {
            // 一旦指定了回調,便調用它們
            feedback(items[item], item + 1, items.Length);
         }
      }
   }
}


class App {
   [STAThreadAttribute]
   static void Main() {
      StaticCallbacks();
      InstanceCallbacks();
   }

   static void StaticCallbacks() {
      // 創建一個Set對象,其中有五個項目
      Set setOfItems = new Set(5);

      // 處理項目,feedback=null
      setOfItems.ProcessItems(null);
      Console.WriteLine();

      // 處理項目,feedback=Console
      setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToConsole));
      Console.WriteLine();

      // 處理項目,feedback =MsgBox
      setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToMsgBox));
      Console.WriteLine();

      // 處理項目,feedback = console AND MsgBox
      Set.Feedback fb = null;
      fb += new Set.Feedback(App.FeedbackToConsole);
      fb += new Set.Feedback(App.FeedbackToMsgBox);
      setOfItems.ProcessItems(fb);
      Console.WriteLine();
   }

   static void FeedbackToConsole(
      Object value, Int32 item, Int32 numItems) {
      Console.WriteLine("Processing item {0} of {1}: {2}.", 
         item, numItems, value);
   }

   static void FeedbackToMsgBox(
      Object value, Int32 item, Int32 numItems) {
      MessageBox.Show(String.Format("Processing item {0} of {1}: {2}.",
        item, numItems, value));
   }

   static void InstanceCallbacks() {
      //創建一個Set對象,其中有五個元素
      Set setOfItems = new Set(5);

      // 處理項目,feedback=File
      App appobj = new App();
      setOfItems.ProcessItems(new Set.Feedback(appobj.FeedbackToFile));
      Console.WriteLine();
   }

   void FeedbackToFile(
      Object value, Int32 item, Int32 numItems) {

      StreamWriter sw = new StreamWriter("Status", true);
      sw.WriteLine("Processing item {0} of {1}: {2}.", 
         item, numItems, value);
      sw.Close();
   }
}
//
    注意代碼最上面的Set類。假設這個類包含一組將被單獨處理的項目。當你創建Set對象時,將它要操縱的項目數傳遞給它的構造函數。然後構造函數再創建對象(Objects)數組並初始化每一個整型值。
    另外,Set類定義了一個公共的委派,這個委派指出某個回調函數的簽名。在這個例子中,委派Feedback 確定了一個帶三個參數的方法(第一個參數爲Object,第二和第三個參數都是Int32類型)並且返回void。在某種意義上,委派很像C/C++中表示某個函數地址的類型定義(typedef)。
    此外,Set類定義了一個公共方法:ProcessItems。這個方法有一個參數feedback——一個對委派Feedback 對象的引用。ProcessItems迭代遍歷所有的數組元素,並且針對每一個元素調用回調方法(由feedback變量指定哪一個會調方法),這個回調方法被傳遞,從而以不同的方式處理回調方法所傳遞的項目值,項目數量以及數組中的元素數目。可以看出回調方法能以它選擇的任何方式處理每一個項目。
使用委派調用靜態方法
    StaticCallbacks方法示範了用各種不同方式的回調委派。這個方法首先構造一個Set對象,告訴它對象創建有五個對象元素的數組。然後調用ProcessItems,在第一個調用中,它的feedback參數爲null。ProcessItems呈現一個方法,這種方法爲每一個Set操縱的項目實現某種動作。在第一個例子中,因爲feedback參數是null,在處理每一個項目時不調用任何回調方法。
    第二個例子中創建了一個新的Set.FeedBack委派對象,這個委派對象是一個方法包裝器,允許方法的調用是經由這個包裝器間接調用。對於類型FeedBack的構造器來說,方法的名字(App.FeedBackConsole)被作爲構造器的參數傳遞;這就表示方法被包裝。然後,從new操作符返回的引用被傳到ProcessItems。現在,當執行ProcessItems時,它會調用App類型的FeedbackToConsole方法處理集合中的每一個項目。FeedbackToConsole簡單地將一個串輸出到控制檯,表示哪個項目被處理了以及這個項目的值是什麼。
    第三個例子與第二個例子基本相同。唯一的差別是Feedback委派對象包裝的是另一個方法:App.FeedbackToMsgBox。這個方法建立一個串,用這個串表示哪個項目被處理以及這個項目的值是什麼。然後將這個串顯示在一個信息框中。
    第四個例子也是靜態調用的最後一個例子示範瞭如何將委派鏈接在一起形成一個鏈。在這個例子中,首先創鍵一個Feedback委派對象的引用變量fb,並將它初始化爲null。這個變量指向委派鏈表的頭。Null值表示鏈表中沒有節點。然後,構造Feedback委派對象,由這個對象包裝對App FeedbackToConsole方法的調用。C#中,+=操作符被用於將對象添加到fb引用的鏈表中。Fb此時指向鏈表的頭。
    最後,構造另一個Feedback委派對象,由這個對象包裝對App FeedbackToMsgBox方法的調用。C#中的+=操作符又一次被用於將對象添加到fb引用的鏈表中,並且fb被新的鏈表的頭更新。現在,當執行ProcessItems時,傳遞給它的是Feedback委派鏈表的頭指針。在ProcessItems內部,調用回調方法的代碼行實際上終止調用所有的在鏈表中由委派對象包裝的回調方法。也就是說,對於被迭代的每一個項目,都會調用FeedbackToConsole,接着馬上調用FeedbackToMsgBox。在後續文章中我將詳細討論委派鏈的處理機制。
    有一點很重要,那就是在這個例子中每件事情都是類型安全的,例如,當構造Feedback委派對象時,編譯器保證App的FeedbackToConsole和FeedbackToMsgBox方法都具備確切的原型,像由Feedback委派定義的一樣。既兩個方法必須有三個參數(Object,Int32和Int32),並且兩個方法必須有相同的返回類型(void)。如果方法原型不匹配,則編譯器將發出下面的出錯信息:“error CS0123:The signature of method ''App.FeedbackToMsgBox()'' does not match this delegate 
type。”——意思是App.FeedbackToMsgBox()方法的簽名與委派的類型不匹配。
調用實例方法
    前面我們討論瞭如何使用委派調用靜態方法。但是委派還能被用於調用特定對象的實例方法。在調用實例方法時,委派需要知道這個它要用方法操作的對象的實例。
    爲了理解實例方法的回調機制,讓我們回頭看看前面代碼中的InstanceCallbacks方法。這段代碼與靜態方法的情形極其相似。注意在Set對象被創建之後,App對象被創建。這個App對象僅僅是創建而已,處於示例目的沒有其它內容。當新的Feedback委派對象被創建的時候,它的構造齊備傳到appobj.FeedbackToFile。這將導致這個委派包裝對FeedbackToFile方法的引用,FeedbackToFile是個實例方法(非靜態)。當這個實例方法被調用時,由appobj引用的對象被操作(作爲隱藏傳遞參數)。FeedbackToFile方法的作用有點像FeedbackToConsole 和 FeedbackToMsgBox,不同的是它打開一個文件並將處理的項目串添加到文件尾。

揭開委派的神祕面紗
    從表面上看,委派好像很容易使用:用C#委派關鍵字定義,用類似new操作符的方式構造它們的實例, 用類似方法調用的語法調用回調方法(不同的是不使用方法名,而是使用指代委派對象的變量)。
    然而,委派的實際運行機制要比前述例子中所描述的過程要複雜的多。編譯器和公共語言運行時(CLR)在幕後所做的許多處理隱藏了這些複雜性,在這一部分中,我們將集中精力來討論編譯器和CLR是如何協同工作實現委派機制的。這些知識將極大地豐富你對委派的理解並且這些知識將告訴你如何有效地使用它們。我們還將涉及到一些在編程中能用到的委派的附加特性。
我們還是從下面這行代碼開始:
public delegate void Feedback(
Object value, Int32 item, Int32 numItems);
當編譯器看到之一行代碼時,它會產生一個完整的類定義,這個定義的代碼會像下面這個樣子:
//
public class Feedback : System.MulticastDelegate {
   // 構造器
   public Feedback(Object target, Int32 methodPtr);

   // 方法與源代碼描述的原型相同
   public void virtual Invoke(
      Object value, Int32 item, Int32 numItems);

   // 方法允許被異步回調,後繼文章將討論這些方法
   public virtual IAsyncResult BeginInvoke(
      Object value, Int32 item, Int32 numItems, 
      AsyncCallback callback, Object object);
   public virtual void EndInvoke(IAsyncResult result);
}
//
事實上,通過使用ILDasm.exe程序檢查結果模塊(如圖三),你能發現編譯器確實自動產生了這個類。
圖三 檢查編譯器產生的類
   在這個例子中,編譯器已經定義了一個叫Feedback的從System.MulticastDelegate類型派生的類,它是在框架類庫(Framework Class Library)中定義的。要知道,所有委派類型都是從MulticastDelegate派生出來的。在這個例子中,Feedback類是公共(public)類型的,因爲在源代碼中它的類型被定義爲public。如果用私有(private)或者受保護的(protected)類型定義,則由編譯器產生的Feedback類也將是私有或者受保護的類型。你應該注意到委派類可能會在某個類中定義(如例子中Feedback就是在Set類中定義的);委派也可能在被定義爲全局型。從本質上說,可以將委派看成是類,可以在定義類的任何地方定義委派。
   因爲所有的委派都派生於MulticastDelegate,它們繼承了MulticastDelegate的域,屬性和方法。在所有這些成員中,你要特別注意三個私有(private)域:
用於委派類型的私有域:
類型 描述
_target System.Object 指回調函數被調用時應該操作的對象。用於實例方法回調
_methodPtr System.Int32 內部整型,CLR用它來標示被回調的方法
_prev System.MulticastDelegate 指另一個委派對象,通常爲null
    所有的委派都有代兩個參數的構造器:一個參數是對象引用,一個是指代回調方法的整型。但是,如果你檢查源代碼,就會發現明白諸如App.FeedbackToConsole 或 appobj.FeedbackToFile的傳遞使用值進行的。你的敏感會告訴你這個代碼不能編譯!
    然而,編譯器知道某個委派被創建,同時編譯器解析源代碼以決定引用哪個對象和方法。對象引用被傳遞爲目標參數,並且用某個特定的Int32值(從某個MethodDef或MethodRef元數據符號獲得)標示的方法被傳遞爲methodPtr參數。對於靜態方法,null被傳遞爲目標參數。在構造器內部,這兩個參數被存儲在它們對應的私有(private)域中。
    另外,構造器將這個域置爲null。這個域被用來創建一個MulticastDelegate對象鏈表。現在我們暫時忽略_prev域,在後續文章中將會詳細討論有關它的內容。
    每一個委派對象實際上就是一個方法包裝器,當方法被調用時,受作用的對象被操作。MulticastDelegate類定義兩個只讀公共實例屬性:Target和Method。給定一個委派對象引用,你就可以查詢到它的這些屬性。如果方法被回調,Target屬性返回一個對將要操作的對象的引用。如果方法是靜態的,則Target返回null。Method屬性返回標示回調方法的System.Reflection.MethodInfo對象。
    你可以用幾種方式使用這些信息。一種方式是檢查是否某個委派對象引用特定類型的實例方法:
//
Boolean DelegateRefersToInstanceMethodOfType(
   MulticastDelegate d, Type type) {

   return((d.Target != null) && d.Target.GetType == type);
}
//
你還應該編寫代碼檢查是否回調方法由專門的名字(如FeedbackToMsgBox):
//
Boolean DelegateRefersToMethodOfName(
   MulticastDelegate d, String methodName) {

   return(d.Method.Name == methodName);
}
//
現在你知道了如何構造委派對象,下面讓我們來談談回調方法是如何被調用的。爲方便起見,我們還是使用
Set類中的ProcessItems:
//
public void ProcessItems(Feedback feedback) {
   for (Int32 item = 1; item <= items.Length; item++) {
      if (feedback != null) {
         // 如果指定任何回調,則調用它們
         feedback(items[item], item, items.Length);
      }
   }
}
//
    註釋行下面的那一行代碼就是調用回調方法。仔細看看代碼,它調用feedback函數並傳遞三個參數。但是feedback是不存在的。再一次指出,編譯器知道feedback是個引用某個委派對象的變量,並且編譯器會產生實際的代碼來調用委派對象的Invoke方法。換句話說,編譯器看到下面這行代碼後:
feedback(items[item], item, items.Length);
編譯器產生的結果與下面這行源代碼產生的結果一樣:
feedback.Invoke(items[item], item, items.Length);
事實上,通過使用ILDasm.exe程序檢查ProcessItems代碼結果(如圖五),你能發現這一點。
圖五 分解後的 Set類的 ProcessItems
    圖五顯示了用於Set類型中ProcessItems方法的微軟中介語言。其中紅色的箭頭指向的指令調用Set.Feedback的Invoke方法。如果你修改源代碼來顯式調用Invoke方法,C#編譯器報錯,出錯信息爲:“error CS1533: Invoke cannot be called directly on a delegate”——意思是Invoke不能針對某個委派被直接調用。C#不允許你顯式調用Invoke(但是,但別的編譯器可以)。
    你會想起當編譯器定義Feedback類的時候,它也定義了Invoke方法。當Invoke被調用時,它使用私有的_target和_methodPtr域來爲特定對象調用希望的方法。注意Invoke方法的簽名與委派的簽名要完全匹配。也就是說,Feedback委派帶三個參數並返回void,那麼Invoke方法也必須帶三個相同的參數並返回void。

結論
    本文討論了有關委派的基本概念。根據本文目前所討論的內容,現在你應該能夠創建並使用它們。在後繼文章中,我將解釋鏈表中的委派鏈,以及一些有關MulticastDelegate的附加方法,System.Delegate類型和事件等......。等着我的好消息吧。
 
本文是系列文章中的第四篇,前面三篇文章分別是:
一、在新的平臺上編程
二、微軟.NET平臺中類型使用的基本原理
三、.NET中的特殊類型成員
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章