1. 程式人生 > 程式設計 >詳解c# 委託鏈

詳解c# 委託鏈

引言:

上一專題介紹了下編譯器是如何來翻譯委託的,從中間語言的角度去看委託,希望可以幫助大家進一步的理解委託,然而之前的介紹都是委託只是封裝一個方法,那委託能不能封裝多個方法呢?因為生活中經常會聽到,我代表大家的意見等這樣的說話,既然委託也是一個代表,那他如果只能代表一個人,那他的魅力就不是很大了吧,所以我們就會委託能不能代表多個方法的? 答案是可以的,這就是本專題要講的內容——委託鏈,委託鏈也是一個委託,只是因為它是把多個委託鏈在一起,所以我們就以委託鏈來這麼稱呼它的。

一、到底什麼是委託鏈

我們平常例項化委託物件時都是繫結一個方法的, 前一個專題介紹的委託也是包裝了一個方法的, 用前面的例子就是委派律師的只有一個人,也就是當事人只有一個的,但是現實生活中顯然不是這樣的,在官司的時候律師可以同時接多個案子,也是接收多個當時人的委派,這樣,該律師就與多個當事人繫結在一起了, 需要了解多個當事人的案件情況的。其實這就是生活中的委託鏈,此時這位律師不僅僅是一個人的代表律師了,而是多個當事人的律師。生活中的委託鏈和C#中的委託鏈很類似的,現在就說說C#中的委託鏈到底是個什麼的?

首先委託鏈就是一個委託,所以大家不要看到委託鏈感覺又是什麼C#中的新特性的,然而要把多個委託鏈在一起,就必須儲存多個委託的引用,那委託鏈物件是在哪裡儲存多個委託的引用的呢?還記得我們上一專題中,我們介紹的委託型別有三個非公共欄位的嗎?這三個欄位是——_target,methodPtr 和_invocationList,至於這三個欄位具體代表什麼大家可以檢視我的上一專題的文章,然而_invocationList 欄位正是儲存多個委託引用的地方的。

為了更好的解釋_invocationList是如何來儲存委託引用的,下面先看一個委託鏈的例子和執行結果,然後再分析原因:

using System;

namespace DelegateTest

{
 public class Program
 {
  // 宣告一個委託型別,它的例項引用一個方法
  // 該方法回去一個int 引數,返回void型別
  public delegate void DelegateTest(int parm);

  public static void Main(string[] args)
  {
   // 用靜態方法來例項化委託
   DelegateTest dtstatic = new DelegateTest(Program.method1);

   // 用例項方法來例項化委託
   DelegateTest dtinstance = new DelegateTest(new Program().method2);
   
   // 隱式呼叫委託
   dtstatic(1);

   // 顯式呼叫Invoke方法來呼叫委託
   dtinstance.Invoke(1);

   // 隱式呼叫委託
   dtstatic(2);

   // 顯式呼叫Invoke方法來呼叫委託
   dtinstance.Invoke(2);
   Console.Read();
  }
  private static void method1(int parm)
  {
   Console.WriteLine("呼叫的是靜態方法,引數值為:" + parm);
  }

  private void method2(int parm)
  {
   Console.WriteLine("呼叫的是例項方法,引數值為:" + parm);
  }
 }

}

執行結果:

詳解c# 委託鏈

下面就來分析下為什麼會出現這樣的結果的:

一開始我們例項化了兩個委託變數,如下程式碼:

 // 用靜態方法來例項化委託
   DelegateTest dtstatic = new DelegateTest(Program.method1);

   // 用例項方法來例項化委託
   DelegateTest dtinstance = new DelegateTest(new Program().method2);

委託變數dtstatic和dtinstance引用的委託物件的初始狀態如下圖:

詳解c# 委託鏈

然後我們定義了一個委託型別的引用變數delegatechain,剛開始它沒有任何委託物件,是一個空引用,當我們執行下面的一行程式碼時,

delegatechain = (DelegateTest)Delegate.Combine(delegatechain,dtstatic);

Combine方法發現試圖合併的是null和dtstatic,在內部,Combine直接返回dtstatic中的物件,此時delegatechain和dtstatic變數引用的都是同一個委託物件,如下圖所示:

詳解c# 委託鏈

這時候,Combine方法發現delegatechain已經引用了一個委託物件了(此時已經引用了destatic引用的委託物件了),所以Combine會構造一個新的委託物件(這一點很想String.Concat,我們簡單的使用是通過+操作符把兩個字串連線起來,這個新的委託物件會對它的私有欄位_target 和_methodPtr欄位進行初始化,然後此時_invocationList欄位初始化為引用了一個委託物件的陣列,這個陣列的第一個元素(下標為0)就是被初始化為引用包裝了method1方法的委託,陣列的二個元素被初始化為引用包裝了method2方法的委託(也就是dtinstance引用的委託物件),最後delegaechain被設為引用新建的這個委託物件,下面是一個圖,可以幫助大家理解委託鏈(也叫多播委託):

詳解c# 委託鏈

同樣的道理,如果是新增第三個委託給委託鏈,過程也是和上面一樣的, 此時又會新建一個委託物件,此時_invocationList欄位會初始化為引用一個儲存這三個委託物件陣列,然而有人會問了——對於已經引用了委託物件的委託型別變數呼叫Combine方法後會建立一個新的委託物件,然後對新的這個委託物件的三個欄位進行重新初始化話,最後把之前的委託型別變數引用新建立的委託物件(這裡就幫大家總結下委託鏈的建立過程),那之前的委託物件怎麼辦呢? 相信大部分人會有這個疑問的,這點和字串的Concat方法很像,之前的委託物件和——invocationList欄位引用的陣列會被垃圾回收掉(正是因為這樣,委託和字串String一樣是不可變的)。

注意:我們還可以呼叫Delegate的Remove方法從鏈中刪除委託,如呼叫下面程式碼時:

delegatechain =(DelegateTest)Delegate.Remove(delegatechain,new DelegateTest(method1));

Remove方法被呼叫時,它會掃描delegateChain(第一個引數)所引用的委託物件內部維護的委託陣列(如果對於委託陣列為空的情況下呼叫Remove方法將不會有任何作用,就是不會刪除任何委託引用,這裡主要是說明掃描是從委託數組裡進行掃描),如果找到delegateChain引用的委託物件的_target和_methodPtr欄位

和第二個引數(新建立的委託)中的欄位匹配的委託,如果刪除之後陣列中只剩下一個數據項時,就返回那個資料項(而不會去新建一個委託物件再初始化的,此時的_invocationList為null,而不是儲存一個委託物件引用的陣列了,具體可以Remove一個後除錯看看的),如果此時陣列中還剩餘多個數據項,就新建一個委託物件——其中建立並初始化_invocationList陣列(此時的陣列引用的委託物件已經少了一個了,因為用Remove方法刪除了),並且,每次Remove方法呼叫只能從鏈中刪除一個委託,而不會刪除有匹配的_target和_methodPtr欄位的所有委託(這個大家可以除錯看看的)

二、如何對委託鏈中的委託呼叫進行控制

通過上面相信大家可以理解如何建立一個委託鏈物件的,但是從執行結果中還可以看出,每次呼叫委託鏈時,委託鏈包裝的每個方法都會順序被執行,如果委託鏈中被呼叫的委託丟擲一個異常,這樣鏈中的後續所有物件都不能被呼叫,並且如果委託的前面具有一個非void的返回型別,則只有最後一個返回值會被保留,其他所有回撥方法的返回值都會被捨棄,這就意味著其他所有操作的返回值都永遠看不到的嗎? 事實卻不是這樣的,我們可以通過呼叫Delegate.GetInvocationList方法來顯式呼叫鏈中的每一個委託,同時可以新增一些自己的定義輸出。

GetInvocationList方法返回一個由Delegate引用構成的陣列,其中每一個數組都指向鏈中的一個委託物件。在內部,GetInvocationList建立並初始化一個數組,讓資料的每一個元素都引用鏈中的一個委託,然後返回對該陣列的一個引用。如果_invocatinList欄位為null,返回的陣列只有一個元素,該元素就是委託例項本身。下面就通過一個程式來演示下的:

namespace DelegateChainDemo
{
 class Program
 {
  // 宣告一個委託型別,它的例項引用一個方法
  // 該方法回去一個int 引數,返回void型別
  public delegate string DelegateTest();

  static void Main(string[] args)
  {
   // 用靜態方法來例項化委託
   DelegateTest dtstatic = new DelegateTest(Program.method1);

   // 用例項方法來例項化委託
   DelegateTest dtinstance = new DelegateTest(new Program().method2);
   DelegateTest dtinstance2 = new DelegateTest(new Program().method3);
   // 定義一個委託鏈物件,一開始初始化為null,就是不代表任何方法(我就是我,我不代表任何人)
   DelegateTest delegatechain = null;
   delegatechain += dtstatic;
   delegatechain += dtinstance;
   delegatechain += dtinstance2;

   ////delegatechain =(DelegateTest)Delegate.Remove(delegatechain,new DelegateTest(method1));
   ////delegatechain = (DelegateTest)Delegate.Remove(delegatechain,new DelegateTest(new Program().method2));
   Console.WriteLine(Test(delegatechain));
   Console.Read();
  }

  private static string method1()
  {
   return "這是靜態方法1";
  }

  private string method2()
  {
   throw new Exception("丟擲了一個異常");
  }

  private string method3()
  {
   return "這是例項方法3";
  }
  // 測試呼叫委託的方法
  private static string Test(DelegateTest chain)
  {
   if (chain == null)
   {
    return null;
   }

   // 用這個變數來儲存輸出的字串
   StringBuilder returnstring = new StringBuilder();

   // 獲取一個委託陣列,其中每個元素都引用鏈中的委託
   Delegate[] delegatearray=chain.GetInvocationList();

   // 遍歷陣列中的每個委託
   foreach (DelegateTest t in delegatearray)
   {
    try
    {
     //呼叫委託獲得返回值
     returnstring.Append(t() + Environment.NewLine);
    }
    catch (Exception e)
    {
     returnstring.AppendFormat("異常從 {0} 方法中丟擲,異常資訊為:{1}{2}",t.Method.Name,e.Message,Environment.NewLine);
    }
   }

   // 把結果返回給呼叫者
   return returnstring.ToString();
  }
 }
}

執行結果截圖:

詳解c# 委託鏈

三、總結下

本專題主要介紹如何建立一個委託鏈以及對於建立一個委託鏈的過程進行了詳細的分享,第二部分主要先指出了委託了一些侷限性,然後通過呼叫GetInvocationList方法來返回一個委託陣列,這樣就可以通過遍歷委託陣列中的每個委託來通知委託的呼叫過程,這樣就可以對委託鏈的呼叫進行更多的控制的。到此本專題也就介紹完了,通過這三個專題對委託的介紹,相信大家會對委託有一個更深的理解,然後為什麼要寫三個專題來詳細介紹委託的呢? 主要是後面要介紹的事件,Lambda表示式,Linq方面的內容都是和委託有關係的,所以更好的理解委託將是後面特性的一個基礎,希望這些對大家有幫助,我將在下一個專題裡面介紹事件。

以上就是詳解c# 委託鏈的詳細內容,更多關於c# 委託鏈的資料請關注我們其它相關文章!