C#再識委託

從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. 定義委託類型

定義一個委託類型,實際上只有一個定義委託的關鍵字、一個類型名稱、一個返回值和參數列表。以下所示:

在這裏值得注意的是,Processor實際上是一個類,只不過看起來像一個方法的簽名,但它不是一個方法,你能夠認爲它是一個特殊的類,但你必定不要說是一個特殊的方法。還有,由於委託是一個類,固然能夠有它的可訪問性修飾符了。編程

2. 定義一個兼容委託類型簽名的回調方法

如今,已經知道了委託類型的簽名,就能夠定義一個兼容於委託類型簽名的回調方法了。

第4種狀況比較特殊,這在C#1.0時代是不容許的,但在C#2.0後是容許的。將一個方法綁定到一個委託時,C#和CLR都容許引用類型的協變性和逆變性。數組

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

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

3.實例化委託類型

在前面,已經有了一個委託類型和一個正確簽名的方法,接着就能夠建立委託的一個實例了,經過委託實例來真正執行這個先前定義的回調方法。在C#中如何建立委託實例,取決於先前定義的方法是實例方法仍是靜態方法。閉包

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

Processor proc1,proc2;
    //靜態方法,類直接調用
    proc1 = new Processor(StaticMethods.PrintString)                   
    InstanceMethods instance = new InstanceMethods();
    //實例方法,經過類的實例調用
    proc2 = new Processor (instance.PrintString)

若是須要真正執行的方法是靜態方法,指定類型名稱就能夠了;若是是實例方法,就須要先建立該方法的類型的實例。這個和平時調用方法是如出一轍的。當委託實例被調用時,就會調用須要真正執行的方法。異步

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

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執行先前定義的回調方法,能夠在這裏向這個執行先前定義的回調方法指定相應參數。能夠用下面這一張圖來解釋:

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查看生成的程序集,驗證編譯器是否真的會自動生成相關代碼,以下圖所示:

在這個例子中,編譯器定義了一個名爲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委託對象,以下圖所示。

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#編譯器完成的。

能夠看出,這是一個至關簡單的轉換過程,但它使得代碼變得整潔多了。

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

一個委託實例調用時,它的全部操做都順序執行。若是委託的簽名具備一個非void的返回值類型,則Invoke的返回值是最後一個操做的返回值。

若是調用列表中的任何操做拋出一個異常,都會阻止執行後續的操做。

2. 深刻委託鏈

委託自己就已經至關有用了,再加上對委託鏈的支持,它的用處就更大了!委託鏈是由委託對象構成的一個集合。利用委託鏈,可調用集合中的委託所表明的所有方法。爲了理解這一點,請參考上面示例代碼中的ChainDelegateDemo1方法。在這個方法中,在Console.WriteLine語句以後,構造了三個委託對象並讓變量fb一、fb2和fb3引用每個對象,以下圖所示:

隨後,我定義了指向Feedback委託對象的引用變量fbChain,並打算讓它引用一個委託鏈或者一個委託對象集合,這些對象包裝了能夠回調的方法。fbChain被初始化爲null,代表目前沒有回調的方法。使用Delegate類的公共靜態方法Combine,能夠將一個委託添加到鏈中:

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

執行以上代碼時,Combine方法會視圖合併null和fb1。在內部,Combine直接返回fb1中的值,因此fbChain變量如今引用的就是fb1變量引用的那個委託對象。以下圖所示:

爲了在鏈中添加第二個委託,再次調用了Combine方法:

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

在內部,Combine方法發現fbChain已經引用了一個委託對象,因此Combine會構造一個新的委託對象。這個新的委託對象對它的私有字段_target和_methodPtr進行初始化,具體值對目前討論的來講並不重要。重要的是,_invocationList字段被初始化爲引用一個委託對象數組。這個數組的第一個元素(索引爲0)被初始化爲引用包裝了FeedbackToConsole方法的委託。數組的第二個元素(索引爲1)被初始化爲引用包裝了FeedbackToMsgBox方法的委託。最後,fnChain被設爲引用新建的委託對象,以下圖所示:

爲了在鏈中添加第三個委託,再次調用了Combine方法:

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

一樣的,Combine方法會發現fbChain已經引用了一個委託對象,因而又Combine會構造一個新的委託對象。這個新的委託對象對它的私有字段_target和_methodPtr進行初始化,具體值對目前討論的來講並不重要。重要的是,_invocationList字段被初始化爲引用一個委託對象數組。這個數組的第一個元素(索引爲0)被初始化爲引用包裝了FeedbackToConsole方法的委託,數組的第二個元素(索引爲1)被初始化爲引用包裝了FeedbackToMsgBox方法的委託,數組的第三個元素(索引爲2)被初始化爲引用包裝了FeedbackToFile方法的委託。最後,fnChain被設爲引用新建的委託對象。注意以前新建的委託以及_invocationList字段引用的數組已經被垃圾回收器回收了。以下圖所示:

在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中爲源代碼中的每一個匿名方法都建立了一個方法:編譯器將在已知類(匿名方法所在的類)的內部生成一個方法,並使用建立委託實例時的行爲,就像它是一個普通的方法同樣。以下圖所示:

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表達式

相關文章
相關標籤/搜索