C#再識委託

weixin_34015860發表於2016-03-23

從C#1到C#3逐步認識委託,由於C#4與C#5對委託改動並不大,故不作說明。

好久沒看.NET了,一直在搞HybridAPP,都忘得差不多了,這也是自己從書中摘下筆跡,供日後翻閱。

C# 1

1.什麼是委託

委託是一種定義方法簽名的型別。當例項化委託時,您可以將其例項與任何具有相容簽名的方法相關聯。 您可以通過委託例項呼叫方法。(MSDN)

  • 委託類似於 C++函式指標,但它們是型別安全的
  • 委託允許將方法作為引數進行傳遞
  • 委託可用於定義回撥方法
  • 委託可以連結在一起
  • 方法不必與委託簽名完全匹配。(協變與逆變)
  • C# 2.0 版引入了匿名方法的概念,此類方法允許將程式碼塊作為引數傳遞,以代替單獨定義的方法。 C#3.0引入了Lambda表示式,利用它們可以更簡練地編寫內聯程式碼塊。匿名方法和 Lambda表示式(在某些上下文中)都可編譯為委託型別

2.如何使用委託

1. 定義委託型別

定義一個委託型別,實際上只有一個定義委託的關鍵字、一個型別名稱、一個返回值和引數列表。如下所示:
362290-20160323151811729-573740979.png
在這裡值得注意的是,Processor其實是一個類,只不過看起來像一個方法的簽名,但它不是一個方法,你可以認為它是一個特殊的類,但你一定不要說是一個特殊的方法。還有,因為委託是一個類,當然可以有它的可訪問性修飾符了。

2. 定義一個相容委託型別簽名的回撥方法

現在,已經知道了委託型別的簽名,就可以定義一個相容於委託型別簽名的回撥方法了。
362290-20160323151945636-2102441356.png
第4種情況比較特殊,這在C#1.0時代是不允許的,但在C#2.0後是允許的。將一個方法繫結到一個委託時,C#和CLR都允許引用型別的協變性和逆變性。

協變性是指方法能返回從委託的返回型別派生的一個型別。逆變性是指方法獲取的引數可以是委託的引數型別的基類。

在委託型別簽名中引數是string型別,根據逆變性,第4個方法的引數完成符合要求。

3.例項化委託型別

在前面,已經有了一個委託型別和一個正確簽名的方法,接著就可以建立委託的一個例項了,通過委託例項來真正執行這個先前定義的回撥方法。在C#中如何建立委託例項,取決於先前定義的方法是例項方法還是靜態方法。

假定在StaticMethods類中的定義一個靜態方法PrintString,在InstanceMethods類中定義一個例項方法PrintString。下面就演示瞭如何如何建立委託型別Processor例項的兩個例子:

    Processor proc1,proc2;
    //靜態方法,類直接呼叫
    proc1 = new Processor(StaticMethods.PrintString)                   
    InstanceMethods instance = new InstanceMethods();
    //例項方法,通過類的例項呼叫
    proc2 = new Processor (instance.PrintString)     

如果需要真正執行的方法是靜態方法,指定型別名稱就可以了;如果是例項方法,就需要先建立該方法的型別的例項。這個和平時呼叫方法是一模一樣的。當委託例項被呼叫時,就會呼叫需要真正執行的方法。

值得注意的是,C#2.0後,可以使用一種簡潔語法,它僅有方法說明符構成,如下所示程式碼。使用快捷語法是因為在方法名稱和其相應的委託型別之間有隱式轉換。

    Processor proc1,proc2;
    proc1 = StaticMethods.PrintString;    //快捷語法
    InstanceMethods instance = new InstanceMethods();
    proc2 = instance.PrintString           //快捷語法

4.呼叫委託

呼叫委託例項指的是呼叫委託例項的一個方法來執行先前定義的回撥方法,不過這顯得非常簡單。如下所示:

   Processor proc1,proc2;
   proc1 = new Processor(StaticMethods.PrintString) //靜態方法,類直接呼叫
   InstanceMethods instance = new InstanceMethods();
   proc2 = new Processor (instance.PrintString)            //例項方法,通過類的例項呼叫
    proc1("PrintString方法執行了");
   //proc1.Invoke("PrintString方法執行了");       
   //proc1("PrintString方法執行了"); 是對proc1.Invoke("PrintString方法執行了"); 的簡化呼叫
    proc2.Invoke("PrintString方法執行了");

值得注意的是,其中的呼叫委託例項的一個方法指的是Invoke方法,這個方法以委託型別的形式出現,並且具有與委託型別的宣告中所指定的相同引數列表和返回型別。所以,在我們的例子中,有一個像下面這樣的方法:

   void Invoke(string input);

呼叫Invoke執行先前定義的回撥方法,可以在這裡向這個執行先前定義的回撥方法指定相應引數。可以用下面這一張圖來解釋:
362290-20160323152112058-1537269156.png

5.完整委託示例

namespace Program {
   //定義委託
   delegate void Processor(string input);
 
   class InstanceMethods
   {
       //定義與委託簽名相同的"例項方法"
       public void PrintString(string message)
       {
           Console.WriteLine(message);
       }
   }
 
   class StaticMethods
   {
       //定義與委託簽名相同的"靜態方法"
       public static void PrintString(string message)
       {
           Console. WriteLine(message);
       }
   }
 
   class Program
   {
       static void Main(string[] args)
       {

           Processor proc1,proc2;
           proc1 = new Processor(StaticMethods. PrintString);   //靜態方法,類直接呼叫
           InstanceMethods instance = new InstanceMethods();
           proc2 = new Processor (instance. PrintString);       //例項方法,通過類的例項呼叫
           proc1("PrintString方法執行了");
           //proc1.Invoke("PrintString方法執行了"); //proc1("PrintString方法執行了")是對proc1.Invoke("PrintString方法執行了")的簡化呼叫
           proc2.Invoke("PrintString方法執行了");
           Console.ReadKey();
       }
   }
}

4.委託的用途

實際上,委託在某種程度上提供了間接的方法。換言之,不需要直接指定一個要執行的行為,而是將這個行為用某種方式“包含”在一個物件中。這個物件可以像其他任何物件那樣使用。在物件中,可以執行封裝的操作。可以選擇將委託型別看做只定義了一個方法的介面,將委託的例項看做實現了這個介面的一個物件。

5.委託揭祕

先看下面一段程式碼,通過這段程式碼,逐步揭祕委託內部。

namespace Test
{
 // 1.宣告委託型別
 internal delegate void Feedback(Int32 value);
 internal class Program
 {
  private static void Main(string[] args)
  {
      StaticDelegateDemo();
      InstanceDelegateDemo();
      ChainDelegateDemo1(new Program());
      ChainDelegateDemo2(new Program());
  }
  private static void StaticDelegateDemo()
  {
      Console.WriteLine("----- Static Delegate Demo -----");
      Counter(1, 3, null);
      // 3.建立委託例項
      Counter(1, 3, new Feedback(Program.FeedbackToConsole));
      Counter(1, 3, new Feedback(FeedbackToMsgBox));
      Console.WriteLine();
  }
  private static void InstanceDelegateDemo()
  {
      Console.WriteLine("----- Instance Delegate Demo -----");
      Program di = new Program();
      // 3.建立委託例項
      Counter(1, 3, new Feedback(di.FeedbackToFile));
      Console.WriteLine();
  }
  private static void ChainDelegateDemo1(Program di)
  {
      Console.WriteLine("----- Chain Delegate Demo 1 -----");
      // 3.建立委託例項
      Feedback fb1 = new Feedback(FeedbackToConsole);
      Feedback fb2 = new Feedback(FeedbackToMsgBox);
      Feedback fb3 = new Feedback(di.FeedbackToFile);
 
      Feedback fbChain = null;
      fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
      fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
      fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
      Counter(1, 2, fbChain);
 
      Console.WriteLine();
      fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
      Counter(1, 2, fbChain);
  }
  private static void ChainDelegateDemo2(Program di)
  {
      Console.WriteLine("----- Chain Delegate Demo 2 -----");
      Feedback fb1 = new Feedback(FeedbackToConsole);
      Feedback fb2 = new Feedback(FeedbackToMsgBox);
      Feedback fb3 = new Feedback(di.FeedbackToFile);
 
      Feedback fbChain = null;
      fbChain += fb1;
      fbChain += fb2;
      fbChain += fb3;
      Counter(1, 2, fbChain);
 
      Console.WriteLine();
      fbChain -= new Feedback(FeedbackToMsgBox);
      Counter(1, 2, fbChain);
  }
 
  private static void Counter(Int32 from, Int32 to, Feedback fb)
  {
      for (Int32 val = from; val <= to; val++)
      {
          // 如果指定了任何回撥,就可以呼叫它
          if (fb != null)
              // 4.呼叫委託
              fb(val);
      }
  }
 
  // 2.宣告簽名相同的方法
  private static void FeedbackToConsole(Int32 value)
  {
      Console.WriteLine("Item=" + value);
  }
 
  // 2.宣告簽名相同的方法
  private static void FeedbackToMsgBox(Int32 value)
  {
      Console.WriteLine("Item=" + value);
  }
 
  // 2.宣告簽名相同的方法
  private void FeedbackToFile(Int32 value)
  {
      StreamWriter sw = new StreamWriter("Status", true);
      sw.WriteLine("Item=" + value);
      sw.Close();
  }
 }
}

從表面看起來,使用一個委託似乎很容易:先用C#的delegate關鍵字宣告一個委託型別,再定義一個要執行的簽名一致的方法,然後用熟悉的new操作符構造委託例項,最後用熟悉的方法呼叫語法來呼叫先前定義的方法。

事實上,編譯器在幕後做了大量的工作來隱藏了不必要的複雜性。首先,讓我們重新認識一下下面的委託型別定義程式碼:

 internal delegate void Feedback(Int32 value);

當編譯器看到這行程式碼時,實際上會生成像下面一個完整的類:

 internal class Feedback: System.MulticastDelegate {
    // 構造器
    public Feedback(object @object, IntPtr method);
    // 這個方法和原始碼指定的原型一樣
    public virtual void Invoke(Int32 value);
    // 以下方法實現了對回撥方法的非同步回撥
    public virtual IAsyncResult BeginInvoke(Int32 value, AsyncCallback callback, object @object);
    // 以下方法獲取了回撥方法的返回值
    public virtual void EndInvoke(IAsyncResult result);
 }

編譯器定義的類有4個方法:一個構造器、Invoke、BeginInvoke和EndInvoke。

現在重點解釋構造器和Invoke,BeginInvoke和EndInvoke看留到後面講解。

事實上,可用.Net Reflector檢視生成的程式集,驗證編譯器是否真的會自動生成相關程式碼,如下圖所示:
362290-20160323152203683-1718705188.png
在這個例子中,編譯器定義了一個名為Feedback的類,該類派生自FCL定義的System.MulticastDelegate型別(所有委託型別都派生自System.MulticastDelegate型別)。

    提示:System.MulticastDelegate類派生自System.Delegate,後則又派生自System.Object。之所以有兩個委託類,是有歷史原因的。
    

從圖中可知Feedback的可訪問性是private,因為委託在原始碼中宣告為internal類。如果原始碼改成使用public可見性,編譯器生成的類也會是public類。要注意,委託類即可巢狀在一個型別中定義,也可以在全域性範圍中定義。簡單地說,由於委託是類,所以凡是能夠定義類的地方,都能定義委託。
由於所有委託型別都派生自MulticastDelegate,所以它們繼承了MulticastDelegate的欄位、屬性和方法。在這些成員中,有三個非公共欄位是最重要的。

欄位 型別 說明
_target System.Object 當委託物件包裝一個靜態方法時,這個欄位為null。當委託物件包裝一個例項方法時,這個欄位引用的是回撥方法要操作的物件。換言之,這個欄位指出了要傳給例項方法的隱式引數this的值
_methodPtr System.IntPtr 一個內部的整數值,CLR用它來標識要回撥的方法
_invocationList System.Object 該欄位通常為null。構造一個委託鏈時,它可以引用一個委託陣列。

注意,所有委託都有一個構造器,它要獲取兩個引數:一個是物件引用,另一個是引用回撥方法的一個整數。然而,如果仔細看下簽名的原始碼,會發現傳遞的是Program.FeedbackToConsole和p.FeedbackToFile這樣的值,還少一個intPtr型別的引數,這似乎不可能通過編譯吧?

然而,C#編譯器知道要構造的是委託,所以會分析原始碼來確定引用的是哪個物件和方法。物件引用被傳給構造器的object引數,標識了方法的一個特殊IntPtr值(從MethodDef或MemberRef後設資料token獲得)被傳給構造器的method引數。對於靜態方法,會為object引數傳遞null值。在構造器內部,這兩個實參分別儲存在_target和_methodPtr私有欄位中。除此之外,構造器還將_invocationList欄位設為null,對這個欄位的討論推遲到後面。

所以,每個委託物件實際都是一個包裝器,其中包裝了一個方法和呼叫該方法時要操作的一個物件。例如,在執行以下兩行程式碼之後:

  Feedback fbStatic = new Feedback(Program.FeedbackToConsole);
  Feedback fbInstance = new Feedback(new Program.FeedbackToFile());

fbStatic和fbInstance變數將引用兩個獨立的,初始化好的Feedback委託物件,如下圖所示。
362290-20160323152240995-1192591071.png

Delegate類定義了兩個只讀的公共例項屬性:Target和Method。給定一個委託物件的引用,可查詢這些屬性。Target屬性返回一個引用,它指向回撥方法要操作的物件。簡單的說,Target屬性返回儲存在私有欄位_target中的值。如果委託物件包裝的是一個靜態方法,Target將返回null。Method屬性返回一個System.Reflection.MethodInfo物件的引用,該物件標識了回撥方法。簡單地說,Method屬性有一個內部轉換機制,能將私有欄位_methodPtr中的值轉換為一個MethodInfo物件並返回它。

可通過多種方式利用這些屬性。例如,可檢查委託物件引用是不是一個特定型別中定義的例項方法:

Boolean DelegateRefersToInstanceMethodOfType(MulticastDelegate d ,Type type) {
    return ((d.Target != null) && d.Target.GetType() == type);
}

還可以寫程式碼檢查回撥方法是否有一個特定的名稱(比如FeedbackToMsgBox):

Boolean DelegateRefersToInstanceMethodOfName(MulticastDelegate d ,String methodName) {
    return (d.Method.Name == methodName);
}

知道了委託物件如何構造並瞭解其內部結構之後,在來看看回撥方法是如何呼叫的。為方便討論,下面重複了Counter方法的定義:

private static void Counter(Int32 from, Int32 to, Feedback fb) {
    for (Int32 val = from; val <= to; val++) {
    // 如果指定了任何回撥,就呼叫它們
        if(fb != null ){
            fb(val); //呼叫委託
        }
    }
}

注意註釋下方的那一行程式碼。if語句首先檢查fb是否為null。如果不為null,下一行程式碼呼叫回撥方法。

這段程式碼看上去是在呼叫一個名為fb的函式,並向它傳遞一個引數(val)。但事實上,這裡沒有名為fb的函式。再次提醒你注意,因為編譯器知道fb是引用了一個委託物件的變數,所以會生成程式碼呼叫該委託物件的Invoke方法。也就是說,編譯器看到以下程式碼時:

  fb(val);

將生成以下程式碼,好像原始碼本來就是這麼寫的:

  fb.Invoke(val);

其實,完全可以修改Counter方法來顯式呼叫Invoke方法,如下所示:

private static void Counter(Int32 from, Int32 to, Feedback fb) {
    for (Int32 val = from; val <= to; val++) {
        // 如果指定了任何回撥,就呼叫它們
        if(fb != null ){
            fb.Invoke(val);
        }
    }
}

前面說過,編譯器是在定義Feedback類時定義Invoke的。所以Invoke被呼叫時,它使用私有欄位_target和_methodPtr在指定物件上呼叫包裝好的回撥方法。注意,Invoke方法的簽名與委託的簽名是匹配的。由於Feedback委託要獲取一個Int32引數,並返回void,所以編譯器生成的Invoke方法也要獲取一個Int32引數,並返回void。

6.委託鏈

1. 委託鏈初印象

委託例項實際有一個操作列表與之關聯。這稱為委託例項的呼叫列表。System.Delegate型別的靜態方法Combine和Remove負責建立新的委託例項。其中,Combine負責將兩個委託例項的呼叫列表連線在一起,而Remove負責從一個委託例項中刪除另一個的委託列表。

委託是不易變的。建立一個委託例項後,有關它的一切就不能改變。這樣一來,就可以安全地傳遞委託例項,並把它們與其他委託例項合併,同時不必擔心一致性、執行緒安全性或者是否其他人檢視更改它的操作。這一點,委託例項和string是一樣的。

但很少在C#中看到對Delegate.Combine的顯式呼叫,一般都是使用+和+=操作符。
圖中展示了轉換過程,其中x和y都是相容委託型別的變數。所有轉換都是由C#編譯器完成的。
362290-20160323152318417-1576877995.png
可以看出,這是一個相當簡單的轉換過程,但它使得程式碼變得整潔多了。

除了能合併委託例項,還可以使用Delegate.Rmove方法從一個例項中刪除另一個例項的呼叫列表。對應的C#簡化操作為-和-=。Delegate.Remove(source,value)將建立一個新的委託例項,其呼叫列表來自source,value中的列表則被刪除。如果結果有一個空的呼叫列表,就返回null。

一個委託例項呼叫時,它的所有操作都順序執行。如果委託的簽名具有一個非void的返回值型別,則Invoke的返回值是最後一個操作的返回值。

如果呼叫列表中的任何操作丟擲一個異常,都會阻止執行後續的操作。

2. 深入委託鏈

委託本身就已經相當有用了,再加上對委託鏈的支援,它的用處就更大了!委託鏈是由委託物件構成的一個集合。利用委託鏈,可呼叫集合中的委託所代表的全部方法。為了理解這一點,請參考上面示例程式碼中的ChainDelegateDemo1方法。在這個方法中,在Console.WriteLine語句之後,構造了三個委託物件並讓變數fb1、fb2和fb3引用每一個物件,如下圖所示:
362290-20160323152351448-313145667.png
隨後,我定義了指向Feedback委託物件的引用變數fbChain,並打算讓它引用一個委託鏈或者一個委託物件集合,這些物件包裝了可以回撥的方法。fbChain被初始化為null,表明目前沒有回撥的方法。使用Delegate類的公共靜態方法Combine,可以將一個委託新增到鏈中:

Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);

執行以上程式碼時,Combine方法會檢視合併null和fb1。在內部,Combine直接返回fb1中的值,所以fbChain變數現在引用的就是fb1變數引用的那個委託物件。如下圖所示:
362290-20160323152419776-1918264637.png
為了在鏈中新增第二個委託,再次呼叫了Combine方法:

fbChain = (Feedback)Delegate.Combine(fbChain, fb2);

在內部,Combine方法發現fbChain已經引用了一個委託物件,所以Combine會構造一個新的委託物件。這個新的委託物件對它的私有欄位_target和_methodPtr進行初始化,具體值對目前討論的來說並不重要。重要的是,_invocationList欄位被初始化為引用一個委託物件陣列。這個陣列的第一個元素(索引為0)被初始化為引用包裝了FeedbackToConsole方法的委託。陣列的第二個元素(索引為1)被初始化為引用包裝了FeedbackToMsgBox方法的委託。最後,fnChain被設為引用新建的委託物件,如下圖所示:
362290-20160323152451058-1881899256.png
為了在鏈中新增第三個委託,再次呼叫了Combine方法:

fbChain = (Feedback)Delegate.Combine(fbChain, fb3);

同樣的,Combine方法會發現fbChain已經引用了一個委託物件,於是又Combine會構造一個新的委託物件。這個新的委託物件對它的私有欄位_target和_methodPtr進行初始化,具體值對目前討論的來說並不重要。重要的是,_invocationList欄位被初始化為引用一個委託物件陣列。這個陣列的第一個元素(索引為0)被初始化為引用包裝了FeedbackToConsole方法的委託,陣列的第二個元素(索引為1)被初始化為引用包裝了FeedbackToMsgBox方法的委託,陣列的第三個元素(索引為2)被初始化為引用包裝了FeedbackToFile方法的委託。最後,fnChain被設為引用新建的委託物件。注意之前新建的委託以及_invocationList欄位引用的陣列已經被垃圾回收器回收了。如下圖所示:
362290-20160323152515089-2065783680.png
在ChainDelegateDemo1方法中,用於設定委託鏈的所有程式碼已經執行完畢,我將fnChain變數交給Counte方法:

Counter(1, 2, fbChain);

Counter方法內部的程式碼會在Feedback委託物件上隱式呼叫Invoke方法,這在前面已經講過了。在fnChain引用的委託上呼叫Invoke時,該委託發現私有欄位_invocationList不為null,所以會執行一個迴圈來遍歷陣列中的所有元素,並依次呼叫每個委託包裝的方法。在本例中,首先呼叫的是FeedbackToConsole,然後是FeedbackToMsgBox,最後是FeedbackToFile。

以虛擬碼的方式,Feedback的Invoke的基本上是向下面這樣實現的:

public void Invoke(Int32 value) {
    Delegate[] delegateSet = _invocationList as Delegate[];
        if (delegateSet != null) {
            foreach(var d in delegateSet)
                d(value);// 呼叫委託
            }else{//否則,不是委託鏈
            _methodPtr.Invoke(value);
        }    
}

注意,還可以使用Delegate公共靜態方法Remove從委託鏈中刪除委託,如下所示。

fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));

Remove方法被呼叫時,它掃描的第一個實參(本例是fbChain)所引用的那個委託物件內部維護的委託陣列(從末尾向索引0掃描)。Remove查詢的是其_target和_methodPtr欄位與第二個實參(本例是新建的Feedback委託)中的欄位匹配的委託。如果找匹配的委託,並且(在刪除之後)陣列中只剩下一個資料項,就返回那個資料項。如果找到匹配的委託,並且陣列中還剩餘多個資料項,就新建一個委託物件——其中建立並初始化_invocationList陣列將引用原始陣列中的所有資料項(刪除的資料項除外),並返回對這個新建委託物件的引用。如果從鏈中刪除了僅有的一個元素,Remove會返回null。注意,每次Remove方法呼叫只能從鏈中刪除一個委託,它不會刪除有匹配的_target和_methodPtr欄位的所有委託。

前面展示的例子中,委託返回值都是void。但是,完全可以向下面這樣定義Feedback委託:

 public delegate Int32 Feedback (Int32 value);

如果這樣定義,那麼該委託的Invoke方法就應該向下面這樣(虛擬碼形式):

public Int32 Invoke(Int32 value) {
    Int32 result;
    Delegate[] delegateSet = _invocationList as Delegate[];
    if (delegateSet != null) {
        foreach(var d in delegateSet)
            result = d(value);// 呼叫委託
        }else{//否則,不是委託鏈
        result = _methodPtr.Invoke(_target,value);
        }
  return result;    
}

1.C#對委託鏈的支援

為方便C#開發人員,C#編譯器自動為委託型別的例項過載了+=和-=操作符。這些操作符分別呼叫了Delegate.Combine和Delegate.Remove。使用這些操作符,可簡化委託鏈的構造。

比如下面程式碼:

Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;

2.取得對委託鏈呼叫更多控制

現在我們已經理解了如何建立一個委託物件鏈,以及如何呼叫鏈中的所有物件。鏈中的所有項都會被呼叫,因為委託型別的Invoke方法包含了對陣列中的所有項進行變數的程式碼。因為Invoke方法中的演算法就是遍歷,過於簡單,顯然,這有很大的侷限性,除了最後一個返回值,其它所有回撥方法的返回值都會被丟棄。還有嗎如果被呼叫的委託中有一個丟擲一個或阻塞相當長的時間,我們又無能為力。顯然,這個演算法還不夠健壯。
由於這個演算法的侷限,所以MulticastDelegate類提供了一個GetInvocationList,用於顯式呼叫鏈中的每一個委託,同時又可以自定義符合自己需要的任何演算法:

public abstract class MulticastDelegate :Delegate {
  // 建立一個委託陣列,其中每個元素都引用鏈中的一個委託
  public sealed override Delegate[] GetInvocationList();
}

GetInvocationList方法操作一個從MulticastDelegate派生的物件,返回一個有Delegate組成的陣列,其中每一個引用都指向鏈中的一個委託物件。
下面是程式碼演示:

public static class GetInvocationList
  {
      // 定義一個 Light 元件
      private sealed class Light
      {
          // 該方法返回 light 的狀態
          public String SwitchPosition()
          {
              return "The light is off";
          }
      }
 
      // 定義一個 Fan 元件
      private sealed class Fan
      {
          // 該方法返回 fan 的狀態
          public String Speed()
          {
              throw new InvalidOperationException("The fan broke due to overheating");
          }
      }
 
      // 定義一個 Speaker 元件
      private sealed class Speaker
      {
          // 該方法返回 speaker 的狀態
          public String Volume()
          {
              return "The volume is loud";
          }
      }
 
      // 定義委託
      private delegate String GetStatus();
 
      public static void Go()
      {
          // 宣告一個為null的委託
          GetStatus getStatus = null;
 
          // 構造三個元件,將它們的狀態方法新增到委託鏈中
          getStatus += new GetStatus(new Light().SwitchPosition);
          getStatus += new GetStatus(new Fan().Speed);
          getStatus += new GetStatus(new Speaker().Volume);
 
          // 輸出該委託鏈中,每個元件的狀態
          Console.WriteLine(GetComponentStatusReport(getStatus));
      }
 
      // 該方法使用者查詢幾個元件的狀態
      private static String GetComponentStatusReport(GetStatus status)
      {
 
          // 如果委託鏈為null,則不進行任何操作
          if (status == null) return null;
 
          // 用StringBuilder來記錄建立的狀態報告
          StringBuilder report = new StringBuilder();
 
          // 獲取委託鏈,其中的每個資料項都是一個委託
          Delegate[] arrayOfDelegates = status.GetInvocationList();
 
          // 遍歷陣列中的每一個委託
          foreach (GetStatus getStatus in arrayOfDelegates)
          {
 
              try
              {
                  // 獲取一個元件的狀態報告,將它新增到StringBuilder中
                  report.AppendFormat("{0}{1}{1}", getStatus(), Environment.NewLine);
              }
              catch (InvalidOperationException e)
              {
                  // 在狀態報告中生成一條錯誤記錄
                  Object component = getStatus.Target;
                  report.AppendFormat(
                     "Failed to get status from {1}{2}{0} Error: {3}{0}{0}",
                     Environment.NewLine,
                     ((component == null) ? "" : component.GetType() + "."),
                     getStatus.Method.Name, e.Message);
              }
          }
 
          // 返回遍歷後的報告
          return report.ToString();
      }
  }

執行結果為:

The light is off

Failed to get status from ConsoleTest.GetInvocationList+Fan.Speed
Error: The fan broke due to overheating

The volume is loud

7.小結

  • 委託封裝了包含特殊返回型別和一組引數的行為,類似包含單一方法的介面。
  • 委託型別宣告中所描述的型別簽名決定了哪個方法可用於建立委託例項,同時決定了呼叫的簽名。
  • 為了建立委託例項,需要一個方法以及(對於例項方法來說)呼叫方法的目標。
  • 委託例項是不易變的。
  • 每個委託例項都包含一個呼叫列表——一個操作列表。
  • 委託例項可以合併到一起,也可以從一個委託例項中刪除一個。
  • 事件不是委託例項——只是成對的add/remove方法。

2.C# 2

2.1 方法組轉換

在C#1中,如果要建立一個委託例項,就必須同時指定委託型別和要採取的操作。如下所示:

   Processor proc1,proc2;
   proc1 = new Processor(StaticMethods. PrintString)   //靜態方法,類直接呼叫
   InstanceMethods instance = new InstanceMethods();
   proc2 = new Processor (instance. PrintString)        //方法,通過類的例項呼叫

為了簡化程式設計,C#2支援從方法組到一個相容委託型別的隱式轉換。所謂"方法組"(method group),其實就是一個方法名。

現在我們可以使用如下程式碼,效果和上面的程式碼一模一樣。

   Processor proc1,proc2;
   proc1 = StaticMethods.PrintString   //靜態方法,類直接呼叫
   InstanceMethods instance = new InstanceMethods();
   proc2 = instance.PrintString        //方法,通過類的例項呼叫

2.2 協變性和逆變性

在前面已經說過C#2.0後,將一個方法繫結到一個委託時,C#和CLR都允許引用型別的協變性和逆變性。

    協變性是指方法能返回從委託的返回型別派生的一個型別。逆變性是指方法獲取的引數可以是委託的引數型別的基類。

2.3 使用匿名方法的內聯委託

1.使用匿名方法

Action

在C#1中,可能一些引數不同,需要建立一個或多個很小的方法,而這些細粒度的方法管理起來又十分不便。在C#2中引入的匿名方法很好的解決了這個問題。

.NET2.0引入了一個泛型委託型別Action,它的簽名非常簡單:

public delegate void Action<T>

Action就是對T的一個例項執行某些操作。例如:

Action<string> printAction1 = delegate(string text){
    char[] chars = text.ToCharArray();
    Array.Reverse(chars);
    Console.WriteLine(new string(chars));
};
Action<int> printAction2 = delegate(int s)
{
    Console.WriteLine(Math.Sqrt(s));
};
private Action printAction3 = delegate
{
    Console.WriteLine("沒有引數");
};
printAction1("asd");
printAction2(4);
printAction3();

上述程式碼展示了匿名方法的幾個不同特性。首先是匿名方法的語法:先是delegate關鍵字,再是引數(如果有的話),隨後是一個程式碼塊,其中包含了對委託例項的操作行定義的程式碼。值得注意的是,逆變性不適用於匿名方法:必須指定和委託型別完全匹配的引數型別。

說到實現,我們在IL中為原始碼中的每個匿名方法都建立了一個方法:編譯器將在已知類(匿名方法所在的類)的內部生成一個方法,並使用建立委託例項時的行為,就像它是一個普通的方法一樣。如下圖所示:
362290-20160323153242120-1755457605.png

2.匿名方法的返回值

Predicate

Action委託的返回型別是void,所以不必從匿名方法返回任何東西。但在需要返回值的情況下怎麼辦呢,這就要使用.NET2.0中的Predicate委託型別。下面是它的簽名:

public delegate bool Predicate<T>(T obj)

從簽名中可以看到,這個委託返回的是bool型別,現在演示一下,建立一個Predicate的一個例項,其返回值指出傳入的實參是奇數還是偶數。

Predicate<int> isEven = delegate (int x) { return x % 2 == 0;};
Console.WriteLine(isEven(1));
Console.WriteLine(isEven(4));

注意:從匿名方法返回一個值時,它始終從匿名函式中返回,而不是從委託例項的方法中返回。

Comparison

Comparison 委託,表示比較同一型別的兩個物件的方法。下面是它的簽名:
public delegate int Comparison(T x,T y)

從簽名中可以看到,這個委託返回的是int 型別。Comparison是在.NET2.0中常見的委託型別,可用它來對集合排序,它是IComparer介面的委託版。通常,一種情況下只需要一個特定的排列順序,所以採取內聯的方式指定完全是合理的,不需要在其餘類的內部新增一個獨立的方法來指定該順序。此委託由 Array 類的 Sort(T[], Comparison) 方法過載和 List 類的 Sort(Comparison)方法過載使用,用於對陣列或列表中的元素進行排序。

internal class Program
      {
          private static void Main(string[] args)
          {
              Program p = new Program();

              SortAndShowFiles("Sorted by name:",delegate (FileInfo f1,FileInfo f2)
              {
                  return f1.Name.CompareTo(f2.Name);
              });

              SortAndShowFiles("Sorted by lenth:", delegate(FileInfo f1, FileInfo f2)
              {
                  return f1.Length.CompareTo(f2.Length);
              });

              Console.Read();
          }


          static void SortAndShowFiles(string title, Comparison<FileInfo> sortOrder)
          {
              FileInfo[] files = new DirectoryInfo(@"C:\").GetFiles();
              Array.Sort(files,sortOrder);
              foreach (var fileInfo in files)
              {
                  Console.WriteLine("{0} ({1} byte)",fileInfo.Name,fileInfo.Length);
              }
          }
      }

3.忽略委託引數

在少數情況下,你實現的委託可能不依賴於它的引數值。你可能想寫一個事件處理程式,它的行為只適用於一個事件,而不依賴事件的實際引數。如下面的例子中,可以完全省略引數列表,只需要使用一個delegate關鍵字,後跟作為方法的操作使用的程式碼塊.

  Button button = new Button();
  button.Test = "Click me";
  button.Click += delegate{ Console.WriteLine("LogClick");};
  button.KeyParess+= delegate{ Console.WriteLine("LogKey");};

一般情況下,我們必須像下面這樣寫:

  button.Click += delegate (object sender, EventArgs e){.....};

那樣會無謂地浪費大量空間——因為我們根本不需要引數的值,所以編譯器現在允許完全省略引數。

4.在匿名方法中捕捉變數

1.定義閉包和不同的變數型別

閉包的基本概念是:一個函式除了能通過提供給它的引數與環境互動之外,還能同環境進行更大程度的互動,這個定義過於抽象,為了真正理解它的應用情況,還需要理解另外兩個術語:
外部變數:指其作用域包括一個函式方法的區域性變數或引數(ref和out引數除外)。在可以使用匿名方法的地方,this引用也被認為是一個外部變數。
被捕捉的外部變數:通常簡稱為被捕獲的變數,它在匿名方法內部使用的外部變數。

重新看一下"閉包"的定義,其中所說的"函式"是指匿名方法,而與之互動的"環境"是指由這個匿名方法捕捉到的變數集合。

它主要強調的是,匿名方法能使用在宣告該匿名方法的方法內部定義的區域性變數。

void EnclosingMethod()
{
    int outervariable = 5;   //外部變數 未捕獲
    string capturedVariable = "captured"; //被匿名方法捕獲的外部變數
    Action x = delegate()
    {
      string anonLocal = "local to anonymous method "; //匿名方法的區域性變數
      Console.WriteLine(anonLocal + capturedVariable); //捕獲外部遍歷
    };
    x();
}

下面描述了從最簡單到最複雜的所有變數:
anonLocal:它是匿名方法的區域性變數,但不是EnclosingMethod的區域性變數
outervariable:它是外部變數,因為在它的作用域內宣告瞭一個匿名方法。但是,匿名方法沒有引用它,所以它未被捕獲。
capturedVariable:它是一個外部變數,因為在它的作用域內宣告瞭一個匿名方法。但是,匿名方法內部引用引用了它,所以它成為了一個被捕獲的變數。

2.測試被捕獲的變數的行為
void EnclosingMethod(){
     string captured = "在x之前建立";

     Action x = delegate{
       Console.WriteLine(captured);
       captured = "被x改變了";
     };

     captured = "在x第一次呼叫之前";
     x();

     Console.WriteLine(captured);

     captured = "在x第二次呼叫之前";
     x();
}

輸出結果:
在x第一次呼叫之前
被x改變了
在x第二次呼叫之前

3.捕獲變數有什麼用

簡單的說,捕獲變數能簡化程式設計,避免專門建立一些類來儲存一個委託需要處理的資訊(作為引數傳遞的資訊除外)。

舉個例子,假定有一個任務列表,並希望寫一個方法來返回包含低於特定年齡的所有人的另一個列表。其中,我們知道List有一個方法能返回一個新列表,這個方法就是FindAll。但是,在匿名方法和捕獲變數問世之前,List.FindAll的存在並沒有多大意義,因為建立一個適合的委託是在太麻煩了。但是在C#2中,這個操作變數非常簡單:

List<Person> Find(List<Person> people,int limit){
  return people.FindAll(delegate(Person person){
     return person.Age < limit; //limit是被捕獲的外部變數
  });
}
4.捕獲變數的延長生命週期

對於一個被捕捉的變數,只要還有任何委託例項在引用它,它就會一直存在。
被捕捉的變數存在於編譯器建立的一個額外的類中,相關的方法會引用該類的例項。

5.區域性變數例項化

當一個變數被捕捉時,捕捉的變數的"例項"。如果在迴圈內捕捉變數,第一迴圈迭代的變數將有別於第二次迴圈時捕獲的變數,以此類推。

6.捕獲變數的使用規則和小結
使用規則
  • 如果用或不用捕獲變數時的程式碼同樣簡單,那就不用
  • 捕捉由for或foreach語句宣告的變數之前,思考你的委託是否需要在迴圈迭代結束之後延續,以及是否想讓它看到那個變數的後續值。否則的話,就在迴圈內另建一個變數,用來複制你想要的值。
  • 如果建立多個委託例項,而且捕獲了變數,思考一下是否希望它們捕獲同一個變數
  • 如果捕獲的變數不會發生改變,那就不要這麼多擔心。
小結
  • 捕獲的變數的生命週期變長了,至少和捕捉它的委託一樣長。
  • 多個委託可以捕獲同一個變數
  • 在迴圈內部,同一個變數宣告實際會引用不同的變數"例項"
  • 在for/foreach迴圈的宣告中建立的變數僅在迴圈持續期間有效
  • 必要時建立額外的型別來儲存捕獲的變數

    5.小結

    C# 2根本性地改變了委託的建立方式,這樣我們就能在.NET Framework的基礎上採取一種更函式化的程式設計風格。

C# 3

1. 作為委託的Lambda表示式

1.Func<T, TResult>

Func<T, TResult> 委託,封裝一個具有一個引數並返回 TResult 引數指定的型別值的方法。下面是它的簽名:

public delegate TResult Func<in T, out TResult>(T arg)

從簽名中可以看到,這個委託返回的是TResult型別。可以使用此委託表示一種能以引數形式傳遞的方法,而不用顯式宣告自定義委託。
封裝的方法必須與此委託定義的方法簽名相對應。也就是說,封裝的方法必須具有一個通過值傳遞給它的引數,並且必須返回值。
在使用 Func<T, TResult>委託時,不必顯式定義一個封裝只有一個引數的方法的委託。
例如,以下程式碼顯式宣告瞭一個名為 ConvertMethod 的委託,並將對UppercaseString方法的引用分配給其委託例項。

using System;
delegate string ConvertMethod(string inString);
public class DelegateExample
{
 public static void Main()
 {
   // Instantiate delegate to reference UppercaseString method
   ConvertMethod convertMeth = UppercaseString;
   string name = "Dakota";
   // Use delegate instance to call UppercaseString method
   Console.WriteLine(convertMeth(name));
 }
 private static string UppercaseString(string inputString)
 {
   return inputString.ToUpper();
 }
}

以下示例簡化了此程式碼,它所用的方法是例項化 Func<T, TResult> 委託,而不是顯式定義一個新委託並將命名方法分配給該委託。

public class GenericFunc
{
 public static void Main()
 {
    // Instantiate delegate to reference UppercaseString method
    Func<string, string> convertMethod = UppercaseString;
    string name = "Dakota";
    // Use delegate instance to call UppercaseString method
    Console.WriteLine(convertMethod(name));
 }

 private static string UppercaseString(string inputString)
 {
    return inputString.ToUpper();
 }
}

您也可以按照以下示例所演示的那樣在 C# 中將 Func<T, TResult> 委託與匿名方法一起使用。

public class Anonymous
{
 public static void Main()
 {
    Func<string, string> convert = delegate(string s)
       { return s.ToUpper();}; 

    string name = "Dakota";
    Console.WriteLine(convert(name));   
 }
}

2.第一次轉換成Lambda表示式

用一個匿名方法來建立委託例項,如:

Func<string,int> returnLength;
returnLength = delegate (string text) { return text.Length; };
Console.WriteLine(returnLength("Hello"));

最終的結果為"5"這是意料之中的事。值得注意的是,returnLength的宣告和賦值是分開的,否則一行可能放不下,這樣還有利於程式碼的理解。
匿名方法是加粗的一部分,也是打算轉換成Lambda表示式的部分。
Lambda表示式最冗長的形式是:
(顯式型別引數列表) => {語句}
=>部分是C#3新增的,他告訴編譯器我們正在使用一個Lambda表示式。Lambda表示式大多數時候都和一個返回非void的委託型別配合使用——如果不返回結果,語法就不像現在這樣一目瞭然了。這標誌著C#1和C#3在用法習慣上的另一個區別。在C#1中,委託一般用於事件,很少會返回什麼。在LINQ中,它們通常被視為資料管道的一部分,接收輸入並返回結果,或者判斷某項是否符合當前的篩選器等等。
這個版本包含了顯式引數列表,並將語句放到大括號中,他看起來和匿名方法非常相似,程式碼如下:

Func<string,int> returnLength;
returnLength = (string text) => { return text.Length; };
Console.WriteLine(returnLength("Hello"));

同樣的,加粗的那一部分是用於建立委託例項的表示式。在閱讀Lambda表示式時,可以將=>部分看錯"goes to"。
匿名方法中控制返回語句的規則同意適用於lambda表示式:如果返回值是void,就不能從Lambda表示式返回一個值;如果有一個非void的返回值型別,那麼每個程式碼路徑都必須返回一個相容的值。

3.用單一表示式作為主題

大多數時候,都可以用一個表示式來表示整個主體,該表示式的值是Lambda的結構。在這些情況下,可以只指定哪個表示式,不使用大括號,不使用return語句,也不新增分號。格式如下:
(顯示型別的引數列表) => 表示式
在這個例子中,Lambda表示式變成了:

 (string text) => text.Length

4.隱式型別的引數列表

編譯器大多數情況下都能猜出引數型別,不需要你顯式宣告它們。在這些情況下,可以將Lambda表示式寫成:
(隱式型別的引數列表) => 表示式
隱式型別的引數列表就是以一個逗號分隔的名稱列表,沒有型別。但隱式和顯式型別的引數不能混合使用——要麼全面是顯式型別引數,要麼全部是隱式型別引數。除此之外,如果有任何out或ref引數,就只能使用顯式型別。在我們的例子中,還可以簡化成:

  (text) => text.Length

5.單一引數的快捷語法

如果Lambda表示式只需要一個引數,而且這個引數可以隱式指定型別,就可以省略小括號。這種格式的Lambda表示式是:
引數名 => 表示式
因此,我們例子中Lambda表示式最紅形式是:

  text => text.Length

值得注意的是,如果願意,可以用小括號將整個Lambda表示式括起來。

6.從匿名方法到Lambda表示式

362290-20160323153324870-456945118.png

相關文章