介紹.NET中的委派(二)

來源:互聯網
上載者:User
在上一篇文章中我介紹了微軟.NET架構中的回調方法:委派(delegates)。解釋了在聲明一個委派後編譯器是如何產生一個從System.MulticastDelegate派生的類,以及這個類是如何建立兩個私人指標域(_target 和 _methodPtr),並用它們來指明回調方法應該操縱哪個對象。我還在上一篇文章中引入了第三個指標域——_prev,這個指標被用來維護委派鏈的鏈表。在本篇文章中,我將把焦點集中在_prev指標域,討論如何管理和使用委派鏈的鏈表。
  回顧委派的一些曆史——System.Delegate 和 System.MulticastDelegate
   .NET架構類庫中定義了System.MulticastDelegate類。在上一篇文章中我們對這個類進行了討論。但是,你應該注意到MulticastDelegate實際上是從System.Delegate(也在.NET架構類庫中定義)類派生而來的,System.Delegate本身派生於System.Object。
   當最初設計.NET架構的時候,微軟的工程師們感覺到需要提供兩種不同種類的委派:即single-cast 和 multicast。MulticastDelegate類型用來表示能被連結在一起的對象,而Delegate類型用來表示不能被連結在一起的對象。System.Delegate類型被設計為基本類型,這個類實現了需要回調某個打包方法的所有功能。MulticastDelegate類從Delegate類中派生並賦予了建立MulticastDelegate對象鏈表(或者說鏈)的能力。
   當編譯原始碼時,編譯器將檢查委派簽名並適當選擇兩個以上的類用於編譯產生委派類型的基類。說起來你會感到稀奇,具有void傳回值的帶簽名的方法會從System.Delegate派生,而具有非void傳回值的方法則從System.MulticastDelegate派生。這樣給人的感覺就是你只能從鏈錶鏈中最後那個方法獲得傳回值。
   在.NET架構beta版測試期間,採用兩種不同的基本類型對開發人員所產生的誤導越來越明顯。此外,用這種方法來設計委派有太多的局限性。例如,許多方法的傳回值在好多情況下都可以被忽略掉。由於這些方法會有非void類型傳回值,他們不會從MulticastDelegate類中派生,防止了它們被組合進某個鏈表。
   為了減少引起的混亂,微軟工程師意圖將Delegate和MulticastDelegate類整合為一個單獨的類,這個類允許任何委派對象參與到某個鏈錶鏈中。所有編譯器會產生從這個單獨的類中派生的委派類。這個變化將降低複雜性,並且將會使.NET架構團隊,通用語言執行平台團隊(CLR),編譯器團隊以及在這個領域使用委派的第三方開發人員從中受益。
   遺憾的是這個整合Delegate和MulticastDelegate的想法在.NET架構開發週期中來得稍遲了一些,況且微軟當時最關心的問題是潛在的Bugs,要求通過測試重現這些Bugs以確定最需要修改的部分。所以在.NET架構的Beta 1中沒有整合這些類。你當然希望在將來的.NET架構版本中將它們整合到一個單獨的類中。
   雖然微軟選擇了延期整合.NET架構類庫中的這兩個類,但他們還是修改了所有的編譯器,以便這些編譯器現在總是從MulticastDelegate類派生來產生委派類型。所以在我的上一篇文章中,我說過這麼一句話:“所有的委派類型都是從MulticastDelegate派生的。”因為編譯器的改動,從而所有委派類型的執行個體都可以被組合到某個鏈錶鏈中而與它們的傳回值無關。
   那麼為什麼要理解這些東西呢?當你開始越來越多地接觸到委派時,你肯定會在.NET架構SDK文檔中碰到Delegate和MulticastDelegate類型。我的意圖是要你理解這兩個類之間的關係。此外,即便是所有你建立的委派類型都以MulticastDelegate作為基類,偶爾你還是會碰到用Delegate所定義的方法來代替MulticastDelegate方法處理自己的委派類型的情況。
   例如,Delegate類的靜態方法是Combine和Remove。(我將會在以後解釋這些方法地用途。)這兩個方法的簽名表示它們帶Delegate參數。因為你的委派類型是從MulticastDelegate派生的,而MulticastDelegate又是從Delegate派生,你的委派類型執行個體可能被傳到Combine和Remove方法。
  比較委派的等同性
   Delegate基類覆蓋了 Object 的虛擬 Equals 方法。MulticastDelegate類型繼承Delegate的Equals實現。Delegate的Equals實現將兩個委派對象進行比較,檢查它們的_target和_methodPtr指標域是否引用相同的對象和方法。如果這兩個指標域相匹配,則Equals返回true,否則Equals返回false。如下面的代碼所示:
  //
  // 構造兩個委派對象,它們引用相同的目標/方法
  //
  Feedback fb1 = new Feedback(FeedbackToConsole);
  Feedback fb2 = new Feedback(FeedbackToConsole);
  
  // 雖然 fb1 和 fb2 引用兩個內部不同的對象,但都引用相同的回調目標/方法
  //
  Console.WriteLine(fb1.Equals(fb2)); // 顯示Displays "True"
  //
  
  此外,Delegate 和 MulticastDelegate都提供相等(==)和不等(!=)操作的重載。因此,你可以使用這些操作符而不用調用Equals方法。下列代碼與上面所列出來的相同
  //
  // 構造兩個委派對象,它們引用相同的目標/方法
  //
  Feedback fb1 = new Feedback(FeedbackToConsole);
  Feedback fb2 = new Feedback(FeedbackToConsole);
  
  // 雖然 fb1 和 fb2 引用兩個內部不同的對象,但都引用相同的回調目標/方法
  //
  Console.WriteLine(fb1==fb2); // 顯示Displays "True"
  //
  
  在處理委派鏈的時候,理解如何比較兩個委派的相等性是很重要的,下面我們就來討論委派鏈。
  委派鏈(Delegate Chains)
   委派本身不但用處很大,而且它還支援委派鏈處理,從而使得它的使用更加舉足輕重。在上一篇文章中,我曾談到每一個MulticastDelegate對象都具備一個私人的_prev域。這個域儲存對另一個MulticastDelegate對象的引用。也就是說,每一個MulticastDelegate物件類型(或者從MulticastDelegate派生的類型)都具備對另一個MulticastDelegate派生對象的引用。這個域允許將委派對象加到某個鏈表中。
   Delegate類定義了三個靜態方法,可以用它們來處理委派對象的鏈錶鏈:
  //
  class System.Delegate {
  // 這個函數聯合由head & tail表示的鏈,head 被返回
  // (注意: head 是最後被調用的委派)
  public static Delegate Combine(Delegate tail, Delegate head);
  
  // 建立有委派數組表示的某個鏈
  // (注意: 入口 0 是 head 並將是最後被調用的委派)
  public static Delegate Combine(Delegate[] delegateArray);
  
  // 從鏈中刪除某個委派匹配值的目標/方法。
  // 韓回新的 head 並將是最後被調用的委派
  public static Delegate Remove(Delegate source, Delegate value);
  }
  //
  
  當你構造一個新的委派對象時,這個對象的_prev域被置為null,表示在鏈表中沒有其它對象。為了將兩個委派組合(combine)到某個鏈表中,可以調用兩個Delegate的靜態方法之一來完成:
  Feedback fb1 = new Feedback(FeedbackToConsole);
  Feedback fb2 = new Feedback(FeedbackToMsgBox);
  Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2);
  // 圖一的左邊顯示了在前面的代碼執行後鏈的狀態
  
  App appobj = new App();
  Feedback fb3 = new Feedback(appobj.FeedbackToStream);
  fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
  
  
  圖一 委派鏈(Delegate Chains)
  圖一展示了所有代碼執行後的鏈。你會注意到 Delegate 類型提供了Combine方法的另一個帶Delegate引用數組為參數的版本。用這個版本的Combine可以向下面一樣重寫前面所列出的代碼:
  Feedback[] fbArray = new Feedback[3];
  fbArray[0] = new Feedback(FeedbackToConsole);
  fbArray[1] = new Feedback(FeedbackToMsgBox);
  App appobj = new App();
  fbArray[2] = new Feedback(appobj.FeedbackToStream);
  
  Feedback fbChain = Delegate.Combine(fbArray);
  
  當某個委派被調用的時候,編譯器產生一個對委派類型Invoke方法的調用。(這在前面的文章中已討論過)為了重新整理記憶體,在前面的文章中曾聲明了一個Feedback委派代碼如下:
  public delegate void
   Feedback(
   Object value, Int32 item, Int32 numItems);
  
  這樣將導致編譯器產生一個包含Invoke方法的Feedback類,如下面的偽碼:
  class Feedback : MulticastDelegate {
   public void virtual Invoke(
   Object value, Int32 item, Int32 numItems) {
  
   // 如果鏈表中還有委派,則應該首先調用它們
   if (_prev != null) _prev.Invoke(value, item, numItems);
  
   // 針對特定的目標對象調用回調方法
   _target.methodPtr(value, item, numItems);
   }
  }
  
  正像你所看到,調用某個委派對象導致其前一個委派被首先調用。當前一個委派返回時,其傳回值被丟棄。在調用其前一個委派之後,這個委派再調用它打包的回調目標/方法。下面的代碼示範了這個過程:
  Feedback fb1 = new Feedback(FeedbackToConsole);
  Feedback fb2 = new Feedback(FeedbackToMsgBox);
  Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2);
  // 這裡,fbChain 引用一個調用FeedbackToMsgBox的委派,而這個委派又引用另一個調用 FeedbackToConsole
  // 的委派,此委派引用null.
  
  // 現在我們調用頭(head)委派,它在內部調用其前面一個委派...
  if (fbChain != null) fbChain(null, 0, 10);
  // 注意: 上面這行代碼中, 必須檢查 fbChain 是否為空白,在調用它之前進行此類檢查是個很好的習慣。
  
   以上是我說明的一個傳回值為 void 的委派 Feedback。如果我像下面這樣定義委派的話:
  public delegate Int32 Feedback(
   Object value, Int32 item, Int32 numItems);
  
  
  class Feedback : MulticastDelegate {
   public Int32 virtual Invoke(
   Object value, Int32 item, Int32 numItems) {
  
   // 只要鏈中有應被首先調用的委派就調用它們
   if (_prev != null) _prev.Invoke(value, item, numItems);
  
  // 調用特定目標對象的回調方法
   return _target.methodPtr(value, item, numItems);
   }
  }
  
   當委派鏈的頭被調用時,它調用鏈中的前一個委派。注意這裡前一個委派的返回置被丟棄。你的應用程式代碼將只能從鏈頭的委派中接收傳回值(即調用的最後一個回調用法)。
   委派對象一旦被創立,它就被認為是不能改變的,也就是說委派對象總是將它們的_prev域置為null,並且不會再變。當把某個委派對象組合到鏈中時,Combine在內部構造一個新的委派對象,它於來源物件有著相同的_target 和 _methodPtr指標域。_prev域被置成鏈中舊的頭。新委派對象的地址從Combine中返回。請看下面的代碼:
  //將委派對象組合到鏈中
  
  Feedback fb = new Feedback(FeedbackToConsole);
  Feedback fbChain = (Feedback) Delegate.Combine(fb, fb);
  // fbChain 引用有兩個委派對象的鏈。其中一個對象與fb所指的對象相同。
  // 另一個對象由Combine構造,這個對象的_prev域引用fb,並且Combine
  // 返回對新對象的引用。
  
  // fb 和 fbChain 引用完全相同的對象嗎?False
  Console.WriteLine((Object) fb == (Object) fbChain);
  
  // fb 和 fbChain 引用相同的回調目標/方法嗎?True
  Console.WriteLine(fb.Equals(fbChain));
  
   現在你知道了如何建立委派鏈,那如何從鏈中刪除一個委派呢?想從某個鏈表中刪除一個委派,必須調用Delegate類型中的靜態Remove方法,參見如下代碼:
  // 調用 Delegate.Remote
  
  Feedback fb1 = new Feedback(FeedbackToConsole);
  Feedback fb2 = new Feedback(FeedbackToMsgBox);
  Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2);
  // fbChain 引用兩個委派的一個鏈
  
  // 調用這個鏈: 兩個方法被調用
  if (fbChain != null) fbChain(null, 0, 10);
  
  fbChain = (Feedback)
   Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
  // fbChain 引用一個委派的鏈
  
  // 調用這個鏈: 一個方法被調用
  if (fbChain != null) fbChain(null, 0, 10);
  
  fbChain = (Feedback)
   Delegate.Remove(fbChain, new Feedback(FeedbackToConsole));
  // fbChain 引用零個委派的鏈(fbChain is null)
  
  // 調用這個鏈: 零個方法被調用
  if (fbChain != null) fbChain(null, 0, 10);
  // 線在你該明白了為什麼我總要判斷 fbChain 是否為null!
  
  這段代碼首先通過構造兩個委派對象來建立一個鏈,然後調用Combine方法將它們組合進一個鏈表中。然後調用Remove方法。Remove的第一個參數是委派對象鏈的頭,第二個參數是要從鏈中刪除的委派對象。這種處理方法很奇怪,為了將委派對象從鏈中刪除,必須先建立新的委派對象。為了明白其中的原因,我們有必要對此進行一些額外解釋。
   在Remove的調用中,我構造了一個新的委派對象。這個委派對象對它自己的_target和_methodPtr域進行初始化,並將_prev置成null。Remove方法掃描鏈(fbChain所指的鏈),檢查鏈中有沒有委派對象與新建立的委派對象相等。要記住,由 Delegate 類實現的並經過重載的Equals 方法只比較_target和_methodPtr域並忽略_prev域。
   如果找到一個匹配的值,Remove 方法則通過固定前一個委派對象的_prev域從鏈中刪除匹配的委派對象。如果沒有找到匹配的值,Remove 方法則什麼也不做(不丟出異常)並返回與它第一個參數相同的值。
   每次對Remove的調用只從鏈中刪除一個對象,參見如下代碼:
  //刪除委派
  
  Feedback fb = new Feedback(FeedbackToConsole);
  Feedback fbChain = (Feedback) Delegate.Combine(fb, fb);
  // fbChain 為兩個委派的鏈
  
  // 調用這個鏈: FeedbackToConsole 被調用兩次
  if (fbChain != null) fbChain(...);
  
  // 從鏈中刪除其中的一個回調
  fbChain = (Feedback) Delegate.Remove(fbChain, fb);
  
  // 調用這個鏈: FeedbackToConsole 被調用一次
  if (fbChain != null) fbChain(...);
  
  // 從鏈中刪除其中的一個回調
  fbChain = (Feedback) Delegate.Remove(fbChain, fb);
  
  // 調用這個鏈: 0 個方法被調用
  if (fbChain != null) fbChain(...);
  //C# 對委派鏈的支援
  
   為了讓C#開發人員易於開發,C#編譯器為委派類型的執行個體自動地提供 += 和 -= 操作的重載。這兩個操作符分別調用Delegate.Combine 和 Delegate.Remove。使用這兩個操作符簡化了委派鏈的建立。下列的C#代碼示範了如何使用C#操作符簡化在一個鏈中對委派對象進行Combine和Remove操作。
  //用 C# 對委派對象進行 組合(Combining)和 刪除(Removing)操作
  
  Feedback fb = new Feedback(FeedbackToConsole);
  App appobj = new App();
  fb += new Feedback(appobj.FeedbackToStream);
  
  // 調用鏈: FeedbackToStream 和 FeedbackToConsole 被調用
  if (fb != null) fb(...);
  
  // 從鏈中刪除一個回調
  fb -= new Feedback(FeedbackToConsole);
  
  // 調用鏈: FeedbackToStream 被調用
  if (fb != null) fb(...);
  
  // 從鏈中刪除最後一個回調
  fb -= new Feedback(appobj.FeedbackToStream);
  
  // 調用鏈: 0 個方法被調用
  if (fb != null) fb(...);
  //
  
   編譯器在內部翻譯所有在委派對象上使用的 += 操作符,使之調用 Delegate 的 Combine 方法。同樣,對於在委派對象上使用的 -= 操作符,編譯器在內部翻譯,使之調用 Delegate 的 Remove 方法。事實上,你可以建立(build)我在這裡列出的代碼,用ILDasm.exe程式看看它的中繼語言是什麼樣子。這樣可以確認C#編譯器到底做了一些什麼事情,你會從中中繼語言中不難看出,編譯器分別用 Delegate 類型中靜態 Combine 和 Remove方法代替了 += 和 -= 操作符。
  調用委派鏈
   前面講述了如何建立一個委派對象的鏈錶鏈,以及如何調用(Invoke)那個鏈中的所有對象。因為委派類型的 Invoke 方法包含了對前一個委派進行(如果有的話)調用的代碼,所以鏈錶鏈中所有的項目都被調用。這種方法確即時非常簡單明了。這種演算法對於大多數遇到的情況都適合,但是它也有許多限制。
   例如,回調方法的傳回值全都被忽略掉了,只留下最後一個。用這種簡單的演算法沒有辦法獲得所有回調方法運行後的傳回值。除此之外,這個演算法還有一些局限性。例如,一旦對某個委派的調用丟出異常或長時間阻塞的話會發生什麼情況呢?因為這個演算法是連續地調用鏈中每個委派,如果對其中某個委派對象出了問題就會影響鏈中其它的委派獲得調用。很顯然,這個演算法並不是一個健壯的演算法。
   為瞭解決這個問題,MulticastDelegate類提供了一個執行個體方法:GetInvocationList,你可以用這個方法顯式地調用鏈中每一個委派,而所使用的演算法可以是任意的:
  //public class MulticastDelegate {
   // 建立一個委派數組;其中每一項都是鏈中項目的複製。
   // (注意: 入口 0 是鏈尾,通常它被首先調用)
   public virtual Delegate[] GetInvocationList();
  }
  //
  
   GetInvocationList 方法操作某個委派鏈的引用並返回一個引用委派對象的數組。GetInvocationList 遍曆指定的鏈並複製鏈中每個對象,將這些複製對象添加到數組中。每個複製都將其自己的_prev域置為null,這樣每個對象被隔離,保證不會與其它的對象鏈搞混。
   這樣一來我們就很容易編寫演算法顯式地調用數組中的每個對象。下面的代碼展示了一種這樣的演算法。
  // GetInvocationList 示範
  
  using System;
  using System.Text;
  
  // 定義一個電燈組件
  class Light {
   // 這個方法返回電燈的狀態
   public String SwitchPosition() { return "The light is off"; }
  }
  
  // 定義一個風扇組件
  class Fan {
   // 這個方法返迴風扇的狀態
   public String Speed() { throw new Exception("The fan broke due to
   overheating"); }
  }
  
  // 定義一個話筒組件
  class Speaker {
   // 這個方法返回話筒的狀態
   public String Volume() { return "The volume is loud"; }
  }
  
  class App {
  
   // 定義委派允許查詢某個組件的狀態
   delegate String GetStatus();
  
   static void Main() {
   // 定義一個空的委派鏈
   GetStatus getStatus = null;
  
   // 構造3個組件並將它們的狀態方法添加到委派鏈中
   getStatus += new GetStatus(new Light().SwitchPosition);
   getStatus += new GetStatus(new Fan().Speed);
   getStatus += new GetStatus(new Speaker().Volume);
  
   // 顯示反映3個組件當時狀態的報告
   Console.WriteLine(GetComponentStatusReport(getStatus));
   }
  
   // 查詢組件狀態的方法及返回某種狀態報表
   static String GetComponentStatusReport(GetStatus status) {
  
   // 如果鏈是空,什麼也不做
   if (status == null) return null;
  
   // 用它來建立狀態報表
   StringBuilder report = new StringBuilder();
  
   // 獲得一個數阻,其元素是鏈中的委派
   Delegate[] arrayOfDelegates = status.GetInvocationList();
  
   // 遍曆數組中的每一個委派
   foreach (GetStatus getStatus in arrayOfDelegates) {
  
   try {
   // 獲得某個組件的狀態串並將它追加到報告中
   report.Append(getStatus() + "\r\n\r\n");
   }
   catch (Exception e) {
   // 在報告中產生本組件出錯入口
   Object component = getStatus.Target;
   report.Append("Failed to get status from " +
   ((component == null) ? "" : component.GetType() + ".") +
   getStatus.Method.Name +
   "\r\n Error: " + e.Message + "\r\n\r\n");
   }
   }
  
   // 將獲得的狀態返回到調用者
   return report.ToString();
   }
  }

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.