程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#基礎知識 >> C#再識委托

C#再識委托

編輯: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實例的兩個例子:

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

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語句之後,構造了三個委托對象並讓變量fb1、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

Func 委托,封裝一個具有一個參數並返回 TResult 參數指定的類型值的方法。下面是它的簽名:

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

從簽名中可以看到,這個委托返回的是TResult類型。可以使用此委托表示一種能以參數形式傳遞的方法,而不用顯式聲明自定義委托。
封裝的方法必須與此委托定義的方法簽名相對應。也就是說,封裝的方法必須具有一個通過值傳遞給它的參數,並且必須返回值。
在使用 Func委托時,不必顯式定義一個封裝只有一個參數的方法的委托。
例如,以下代碼顯式聲明了一個名為 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 委托,而不是顯式定義一個新委托並將命名方法分配給該委托。

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 委托與匿名方法一起使用。

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved