回調函數是一種很是有用的編程機制,它的存在已經有不少年了。.NET經過委託來提供回調函數機制。不一樣於其餘平臺(好比非託管C++)的回調機制,委託的功能要多得多。例如,委託確保回調方法是類型安全的(這是clr最重要的目標之一)。委託還容許順序調用多個方法,並支持調用靜態方法和實例方法。程序員
c「運行時」的qsort函數獲取指向一個回調函數的指針,一遍對數組中的元素進行排序。在windows中,窗口過程、鉤子過程和異步過程調用等都須要回調函數。.net framework中,回調方法的應用更是普遍。例如,能夠登記回調方法來獲取各類各樣的通知,例如未處理的異常、窗口狀態變化、菜單項選擇、文件系統變化、窗體控制事件和異步操做已完成等。算法
在非託管C/C++中,非成員函數的地址只是一個內存地址。這個地址不攜帶任何額外信息,。好比函數指望收到的參數個數、參數類型、函數返回值類型以及函數調用協定。簡單地說,非託管C/C++回調函數不是類型安全的(不過他們確實是一種很是輕量級的機制)。編程
.NET的回調函數和非託管windows編程環境的回調函數同樣有用,同樣廣泛。可是,.net提供了稱爲委託的類型安全機制。爲了理解委託,先來看看如何使用它。c#
委託4個最基本的步驟:windows
1)定義委託類型數組
2)有一個方法包含要執行的代碼(簽名要與委託相同)安全
3)建立一個委託實例化(包含聲明委託對象)app
4)執行調用(invoke)委託實例異步
具體解釋以下:編程語言
1.定義委託類型
委託類型就是參數類型的一個列表以及一個返回類型。
delegate void StringProcessor(string input);
其中的StringProcessor是一個類型。
2.定義簽名相同的方法
定義的方法要與委託有類型相同的返回值和參數。
private void GetStringLength(object x){} //C#2.0之後認爲一致
3.建立委託實例
建立委託實例就是指定在調用委託實例時執行的方法。
StringProcessor proc1,proc2 //GetStringLength 實例方法 proc1= new StringProcessor(GetStringLength); //GetString 靜態方法 proc2 += GetString;
4.調用委託
調用委託就是調用一個委託實例方法。
proc1("Hello World");
如下代碼演示瞭如何聲明、建立和使用委託。
// 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(); } }
在上面的代碼中,咱們能夠清楚的看到用委託如何回調靜態方法。直接將靜態方法綁定到委託的實例上,再經過實例進行調用。
理解counter方法的設計及其工做方式以後,再來看看如何利用委託回調靜態方法。
在StaticDelegateDemo方法中第二次調用counter,爲第三個參數傳遞新構造的feedback委託對象。委託對象是方法的包裝器(wrapper),使方法能經過包裝器來間接回調。本例中,靜態方法的完整名稱Program.FeedbackToConsole被傳給feedback委託類型的構造器,這就是要包裝的方法。
注意:FeedbackToConsole方法被定義成Program類型內部的私有方法,但counter方法能調用Program類型的私有方法。這明顯沒有問題,由於counter和FeedbackToConsole在同一個類型中定義,即便不在同一個類型,也不會出問題,只要feedback委託對象是有具備足夠安全性/可訪問性的代碼建立的,便沒有問題。
將方法綁定到委託時,C#和CLR都容許引用類型的協變性和逆變性。協變性是指方法能返回從委託的返回類型派生的一個類型。逆變性是指方法獲取的參數能夠是委託的參數類型的基類。例以下面的委託:
deleget Object MyCallback(FileStream s);
徹底能夠構造該委託類型的一個實例,並和具備一下原型的一個方法綁定:
String SomeMethod(Stream s);
在這裏,SomeMethod的返回類型(String)派生自委託的返回類型(Object);這種協變性是容許的。SomeMethod的參數類型(Stream)是委託的參數類型(FileStream)的基類;這種逆變性是容許的。
注意,協變性和逆變性只能用於引用類型,不能做用於值類型和void。因此下面示例是錯誤的:
Int32 SomeMethod(Stream s);//這是錯誤的
值類型和void之因此不支持協變性和逆變性,是由於它們的存儲結構是變化的,而引用類型的存儲結構始終是一個指針。
用委託回調實例方法
使用委託回調實例方法,在上面代碼中演示已經很是清楚了,就不細說了。
包裝實例方法頗有用,由於對象內部代碼能夠訪問對象的實例成員。這意味着對象能夠維護一些狀態,並在回調方法執行期間利用這些信息。
從表面看,委託彷佛很容易使用:用C#的delegate關鍵字聲明,用熟悉的new操做符構造委託實例,用熟悉的方法調用語法來調用回調函數(用引用了委託對象的變量替代方法名)。
然而,實際狀況遠比前面例子演示的複雜的多。編譯器和CLR在幕後作了大量工做來隱藏複雜性。本節重點講解了編譯器和CLR如何協同工做來實現委託。掌握這些知識有助於加深對委託的理解,並學會如何更高效地使用。另外,還要介紹經過委託來實現的一些附加功能。
首先讓咱們重寫審視下面的代碼:
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看留到後面講解。
事實上,可用ILDasm.exe查看生成的程序集,驗證編譯器真的會自動生成這個類,如圖17-1所示:
在這個例子中,編譯器定義了一個名爲Feedback的類,該類派生自FCL定義的System.MulticastDelegate類型(全部委託類型都派生自System.MulticastDelegate類型)。
提示:System.MulticastDelegate類派生自System.Delegate,後則又派生自System.Object。之因此有兩個委託類,是有歷史緣由的。
從圖中可知Feedback的可訪問性是private,由於委託在源代碼中聲明爲internal類。若是源代碼改爲使用public可見性,編譯器生成的類也會是public類。要注意,委託類便可嵌套在一個類型中定義,也能夠在全局範圍中定義。簡單地說,因爲委託是類,因此凡是可以定義類的地方,都能定義委託。
因爲全部委託類型都派生自MulticastDelegate,因此它們繼承了MulticastDelegate的字段、屬性和方法。在這些成員中,有三個非公共字段是最重要的。
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); }
注意,全部委託都有一個構造器,它要獲取兩個參數:一個是對象引用,另外一個是引用回調方法的一個整數。然而,若是仔細看下簽名的源代碼,會發現傳遞的是Program.FeedbackToConsole和di.FeedbackToFile這樣的值,這彷佛不可能經過編譯吧?
然而,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委託對象,如圖17-2所示。
知道了委託對象如何構造並瞭解其內部結構以後,在來看看回調方法是如何調用的。爲方便討論,下面重複了Counter方法的定義:
private static void Counter(Int32 from, Int32 to, Feedback fb) { for (Int32 val = from; val <= to; val++) { // 若是指定了任何回調,就調用它們 if(fb != null ){ fb(val); } } }
這裏的null檢查必不可少,由於fb知識可能引用了feedback委託對象的變量,他也可能爲null。這段代碼看上去像是調用了一個名爲fb的函數,並向它傳遞一個參數(val)。但事實上,這裏沒有名爲fb的函數。再次提醒你注意,由於編譯器知道fb是引用了委託對象的變量,因此會生成代碼調用該委託對象的Invoke方法。也就是也就是還說,編譯器在看到如下代碼時:
fb(val);
將生成如下代碼,好像源代碼原本就是這麼寫的:
fb.Invoke(val);
爲了驗證編譯器生成的代碼來調用委託類型的Invoke方法,可利用ILDasm.exe來檢查生成的IL代碼:
.method private hidebysig static void Counter(int32 from,int32 'to',class ConsoleTest.Feedback fb) cil managed { // 代碼大小 41 (0x29) .maxstack 2 .locals init ([0] int32 val, [1] bool CS$4$0000) IL_0000: nop IL_0001: ldarg.0 IL_0002: stloc.0 IL_0003: br.s IL_001d IL_0005: nop IL_0006: ldarg.2 IL_0007: ldnull IL_0008: ceq IL_000a: stloc.1 IL_000b: ldloc.1 IL_000c: brtrue.s IL_0018 IL_000e: nop IL_000f: ldarg.2 IL_0010: ldloc.0 IL_0011: callvirt instance void ConsoleTest.Feedback::Invoke(int32) IL_0016: nop IL_0017: nop IL_0018: nop IL_0019: ldloc.0 IL_001a: ldc.i4.1 IL_001b: add IL_001c: stloc.0 IL_001d: ldloc.0 IL_001e: ldarg.1 IL_001f: cgt IL_0021: ldc.i4.0 IL_0022: ceq IL_0024: stloc.1 IL_0025: ldloc.1 IL_0026: brtrue.s IL_0005 IL_0028: ret } // end of method Program::Counter
其實,徹底能夠修改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。
委託自己就已經至關有用了,在加上對委託鏈的支持,它的用處就更大了!委託鏈是由委託對象構成的一個集合。利用委託鏈,可調用集合中的委託所表明的所有方法。爲了理解這一點,請參考第一節中的示例代碼中的ChainDelegateDemo1方法。在這個方法中,在Console.WriteLine語句以後,我構造了三個委託對象並讓變量fb一、fb2和fb3引用每個對象,如圖17-3所示:
指向Feedback委託對象的引用變量fbChain旨在引用委託鏈,這些對象包裝了能夠回調的方法。fbChain被初始化爲null,代表目前沒有回調的方法。使用Delegate類的公共靜態方法Combine,能夠將一個委託添加到鏈中:
Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
執行以上代碼時,Combine方法會視圖合併null和fb1。在內部,Combine直接返回fb1中的值,因此fbChain變量如今引用的就是fb1變量引用的那個委託對象。如圖17-4所示:
再次調用了Combine方法,在鏈中添加第二個委託:
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
在內部,Combine方法發現fbChain已經引用了一個委託對象,因此Combine會構造一個新的委託對象。這個新的委託對象對它的私有字段_target和_methodPtr進行初始化,具體值對目前討論的來講並不重要。重要的是,_invocationList字段被初始化爲引用一個委託對象數組。這個數組的第一個元素(索引爲0)被初始化爲引用包裝了FeedbackToConsole方法的委託。數組的第二個元素(索引爲1)被初始化爲引用包裝了FeedbackToMsgBox方法的委託。最後,fnChain被設爲引用新建的委託對象,如圖17-5所示:
爲了在鏈中添加第三個委託,再次調用了Combine方法:
fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
一樣的,Combine方法會發現fbChain已經引用了一個委託對象,因而又Combine會構造一個新的委託對象。這個新的委託對象對它的私有字段_target和_methodPtr進行初始化,具體值對目前討論的來講並不重要。重要的是,_invocationList字段被初始化爲引用一個委託對象數組。這個數組的第一個元素(索引爲0)被初始化爲引用包裝了FeedbackToConsole方法的委託,數組的第二個元素(索引爲1)被初始化爲引用包裝了FeedbackToMsgBox方法的委託,數組的第三個元素(索引爲2)被初始化爲引用包裝了FeedbackToFile方法的委託。最後,fnChain被設爲引用新建的委託對象。注意以前新建的委託以及_invocationList字段引用的數組已經被垃圾回收器回收了。如圖17-6所示:
在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; }
數組中的每一個委託被調用時,其返回值被保存到result變量中。循環完成後,result變量只包含調用的最後一個委託的結果(前面的返回值會被丟棄),該值返回給調用Invoke的代碼。
爲方便C#開發人員,C#編譯器自動爲委託類型的實例重載了+=和-=操做符。這些操做符分別調用了Delegate.Combine和Delegate.Remove。使用這些操做符,可簡化委託鏈的構造。
好比下面代碼:
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
此時,想必你已經理解了如何建立委託對象,以及如何調用鏈中的全部對象。鏈中的全部項都會被調用,由於委託類型的invoke方法包含了對數組中全部項進行遍歷的代碼。由於Invoke方法中的算法就是遍歷,過於簡單,顯然,這有很大的侷限性,除了最後一個返回值,其它全部回調方法的返回值都會被丟棄。但侷限並不止於此。若是被調用的委託中有一個拋出一個異常或阻塞至關長的時間,會出現什麼狀況?因爲這個簡答的算法是順序調用鏈中的每個委託,因此一個委託對象出現問題,鏈中後續的全部對象都調用不了。顯然,這個算法還不夠健壯。
因爲這個算法的侷限,因此MulticastDelegate類提供了一個實例方法GetInvocationList,用於顯式調用鏈中的每個委託,同時又能夠自定義符合本身須要的任何算法:
public abstract class MulticastDelegate :Delegate { // 建立一個委託數組,其中每一個元素都引用鏈中的一個委託 public sealed override Delegate[] GetInvocationList(); }
GetInvocationList方法操做從MulticastDelegate派生的對象,返回包含Delegate引用的一個數組,其中每個引用都指向鏈中的一個委託對象。在內部,GetInvocationList構造並初始化一個數組,讓它的每一個元素都引用鏈中的一個委託,而後返回對該數組的引用。若是_invocationList字段爲null,返回的數組就只有一個元素,該元素引用鏈中惟一的委託,即委託實例自己。
能夠很容易地寫一個算法來顯示調用數組中每一個對象,下面是代碼演示:
// 定義一個 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 Main() { // 聲明一個爲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
許多年前,.NET Framework剛開始開發時,Microsoft引入委託的概念。開發人員在FCL中添加類時,他們在引入了回調方法的全部定法定義新的委託類型。隨着時間的推移,他們定義了太多的委託。事實上,如今僅僅在MSCorLib.dll中,就有接近50個委託類型。好比:
public delegate void TryCode(Object userData);
public delegate void WaitCallback(Object state);
public delegate void TimerCallback(Object state);
...
你發現上面幾個委託的共同點了嗎?它們其實全是同樣的:這些委託類型的變量引用的方法都是獲取一個Object,而且返回void。沒有理由定義這麼多委託類型,定義一個就行了!
如今,.NET Framewoke如今支持泛型,因此實際上只須要幾個泛型委託就能夠表示獲取多達16個參數的方法:
public delegate void Action(); //這不是泛型
public delegate void Action<T>(T obj);
public delegate void Action<T1,T2>(T1 obj1,T2 obj2);
public delegate void Action<T1,T2,T3>(T1 obj1,T2 obj2,T3 obj3);
...
public delegate void Action<T1,...,T16>(T1 obj1,...,T16 obj16);
因此,.NET Framework如今提供17個Action委託,它們從無參數一直到最多16個參數。若是方法須要獲取16個意思、上的參數,就必須定義本身的委託類型,但這種狀況應該是極其罕見的。除了Action委託,.NET Framewoke還提供了17個Func函數,它們容許回調方法方法返回一個值:
public delegate TResult Func<TResult>();
public delegate TResult Func<T,TResult>(T1 arg);
public delegate TResult Func<T1,T2,TResult>(T1 arg1,T2 arg2);
...
public delegate TResult Func<T1,...,T16,TResult>(T1 arg1,...,T16 arg16);
建議儘可能使用這些委託類型,而不是在代碼中定義更多的委託類型。這樣能夠減小系統中的類型數目,同時簡化編碼。然而,若是須要使用ref或out關鍵字,以引用的方式傳遞一個參數,就可能不得不定義本身的委託:
delegate void Bar(ref Int32 z);
使用獲取泛型實參和返回值的委託時,可利用逆變和協變,並且建議你老是利用這些功能,由於它們沒有反作用,並且是你的委託適用於更多情形。
許多開發人員認爲和委託打交道很麻煩。由於它的語法很奇怪。例如如下代碼:
button1.Click += new EventHandle(button1_Click);
其中的button1_Click是一個方法,它看起來像下面這樣:
void button1_Click(Object sender, EventArgs e) { // 按鈕單擊後要作的事情.... }
第一行代碼的思路是向按鈕控件登記button1_Click方法的地址,以便在該按鈕被單擊時,能夠調用方法。許多開發人員認爲,僅僅爲了指定button1_CLick方法的地址,就構造一個EventHandle委託對象,這顯得有點難以想象。然而,構造EventHandle委託對象是CLR要求的,由於這個對象提供了一個包裝器,可確保(被包裝的)方法只能以類型安全的方式調用。這個包裝器還支持調用實例方法和委託鏈。可是不少開發人員不想研究這些細節,更喜歡像下面這樣的寫代碼:
button1_Click += button1_Click;
幸虧,C#編譯器爲開發人員提供了一些用於處理委託的簡化方法。後文描述的實際上可歸爲C#的語法糖(syntactical sugar)。這些簡化語法爲程序員提供了一種更簡答的方式生成clr和其餘編程語言處理委託時所必須的il代碼。
如前所示,C#容許指定回調方法的名稱,沒必要構造一個委託對象包裝器。例如:
public sealed class AClass { private static void CallbackWithoutNewingADelegateObject(){ ThreadPool.QueueUserWorkItem(SomeAsyncTask,5); } private static void SomeAsyncTask(Object o) { Console.WriteLine(o); } }
這裏,ThreadPool類的靜態方法QueueUserWorkItem指望接受對一個WaitCallback委託對象的引用,委託對象中包裝的是對SomeAsyncTask方法的一個引用。因爲C#編譯器可以本身進行推斷,因此能夠省略構造WaitCallback委託對象的代碼,使整個代碼的可讀性更強,也更容易理解。固然,當代碼編譯時,C#編譯器會生成IL代碼來構建WaitCallback委託對象——咱們只是在語法上獲得了簡化而已。
在前面的代碼中,是將回調方法SomeAsyncTask傳給ThreadPool的QueueUserWorkItem方法。C#容許咱們之內聯的方式寫回調方法的代碼。沒必要再另外定義方法寫。例如,前面的代碼能夠重寫爲下面這樣:
public sealed class AClass { private static void CallbackWithoutNewingADelegateObject(){ ThreadPool.QueueUserWorkItem(SomeAsyncTask,5); } private static void SomeAsyncTask(Object o) { Console.WriteLine(o); } }
注意,傳給QueueUserWorkItem方法的第一個實參實際上是一個lambda表達式。經過C# limbda表達式操做符=>,能夠很容易地識別這種表達式。lambda表達式可在編譯器預計須要一個委託的地方使用。編譯器看到這個lambda表達式以後,會在類中自動建立一個新的私有方法。這個新方法成爲匿名函數(anonymous function),由於方法的名稱是編譯器自動建立的,開發人員通常不知道這個名稱。經過ILDasm.exe查看C#編譯器將該方法命名爲了<CallbackWithoutNewingADelegateObject>b__0,它獲取一個Object參數,返回void.
編譯器選擇的方法名以<符號開頭,這是由於在C#中,標識符是不能包含<符號的;這就確保了你不會碰巧定義一個編譯器自動選擇的名稱。順便說一句,雖然C#禁止標識符包含<符號,可是CLR容許,這也就是爲何編譯不會出錯的緣由了。另外注意,雖然可將方法名做爲一個字符串來傳遞,經過反射來訪問方法,可是C#語言規範指出,編譯器生成名稱的方式是沒有任何保證的。例如,每次編譯代碼,編譯器生成的方法均可能是一個不一樣的名稱。
經過ILDasm.exe,咱們還注意到C#編譯器向這個方法應用了一個名爲System.Runtime.CompilerServices.CompilerGeneratedAttribute的attribute,指出方法是編譯器生成的,而非開發人員定義的。=>操做符右側的代碼被放入這個編譯器生成的方法中。
注意:C#2.0面世時,它引入了一個稱爲匿名方法的功能。和C#3.0引入的lambda表達式類似,匿名方法描述的也是用於建立匿名函數的一個語法。C#語言規範建議開發人員使用新的lambda表達式,而不要使用舊的匿名方法語法,由於lambda表達式語法更簡潔,代碼更容易寫、讀和維護。
注意:寫lambda表達式時沒有辦法向編譯器生成的方法引用定製特性。此外,不能向方法應用任何方法修飾符(好比unsafe)。但這一版不會有什麼問題,由於編譯器生成的匿名函數老是私有方法,並且要麼是靜態的,要麼是非靜態的,具體取決於方法是否訪問了任何實例成員,因此,不必向方法應用public之類的修飾符。
最後,若是卸載前面的代碼編譯,c#編譯器會將這些代碼改寫爲下面:
lambda表達式必須匹配waitCallback委託:獲取一個object並返回void。但在指定參數名稱時,我簡單地將obj放在=>操做符左側。在=>操做符右側,返回void。然而,若是在這裏放一個返回值不爲void的表達式,編譯器生成的代碼會直接忽略返回值,由於編譯器生成的方法必須用void返回類型來知足waitCallback委託。
另外還要注意,匿名函數被標記爲private,禁止非類型內定義的代碼訪問(儘管反射能揭示出方法確實存在)。另外,匿名函數被標記爲static,由於代碼沒有訪問任何實例成員(也不能訪問,由於CallbackWithoutNewingADelegateObject自己是靜態方法)。若是CallbackWithoutNewingADelegateObject方法不是靜態的,匿名函數的代碼就能夠包含對實例成員的引用。不包含實例成員引用,編譯器仍會生成靜態匿名函數,由於它的效率比實例方法搞。之因此更高效,是由於不須要額外的this參數,可是,若是匿名函數的代碼確實引用了實例成員,編譯器就會生成非靜態匿名函數。
=>操做符左側指定傳給lambda表達式的參數的名稱。下例總結了一些規則:
//若是不須要返回值 使用action Action a = () => Console.WriteLine("123123"); //若是委託不獲取任何參數,就是用() Func<string> f = () => "jeff"; //若是委託獲取1個或更多參數,可顯示指定類型 Func<int,string> f2=(int n)=> n.ToString(); //若是委託獲取1個或更多參數,編譯器可推斷類型,並且能夠省略括號 Func<int,string> f3=n=> n.ToString(); //若是委託有ref或out參數,必須顯式指定ref、out和類型 Bar b = (out int n) => n = 5;
若是主題由兩個或多個語句構成,必須用大括號將語句封閉。在用了大括號的狀況下,若是委託預期返回值,還必須在主體中添加return語句。
提示:lambda表達式的主要優點在於,它從你的源代碼中移除了一個"間接層"。或者說避免了迂迴,正常狀況下,必須寫一個單獨的方法,命名該該按方法,再在須要委託的地方傳遞這個方法。方法名提供了引用代碼主題的一種方式,若是要在多個地方引用同一個代碼主題,單獨寫一個方法並命名確實是理想方案。但若是隻須要在代碼中引用一次,那麼lambda表達式容許直接內聯那些代碼,沒必要爲它分配名稱,提升了編程效率。
前面展現了回調代碼如何引用類中定義的其餘成員。但有時候,還但願回調代碼引用存在於方法中的局部參數或變量。下面有個有趣的例子:
internal sealed class AClass2 { internal static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo) { // 一些局部變量 Int32[] squares = new Int32[numToDo]; AutoResetEvent done = new AutoResetEvent(false); // 在其它線程上執行一系列任務 for (Int32 n = 0; n < squares.Length; n++) { ThreadPool.QueueUserWorkItem( delegate(Object obj) { Int32 num = (Int32)obj; // 耗時任務 squares[num] = num * num; // 若是是最後一個任務,則讓主線程繼續執行 if (Interlocked.Decrement(ref numToDo) == 0) done.Set(); }, n); } // 等待其餘全部線程執行完畢 done.WaitOne(); // 顯示任務 for (Int32 n = 0; n < squares.Length; n++) Console.WriteLine("Index {0}, Square={1}", n, squares[n]); } }
這個例子實際演示了C#如何簡單的實現一個很是複雜的任務。以上方法定義了一個參數numToDo和兩個局部變量aquares和done,並且lambda表達式的主體引用了這些變量。如今,想象如下lambda表達式主體中的代碼在一個單獨的方法中(事實上,也的確如此)。變量的值如何傳遞給這個單獨的方法呢?惟一的方法就是定義一個新的輔助類,這個類要爲咱們打算傳給回調代碼的每個值都定義一個字段。此外,回調代碼還必須定義這個輔助類中的一個實例方法。而後,UsingLocalVariablesInTheCallbackCode方法必須構造輔助類的一個實例,用方法定義的局部變量的值來初始化這個實例中的字段。而後,構造綁定到輔助對象/實例方法的委託對象。
注意:當lambda表達式形成編譯器生成一個類時,並且參數/局部變量被轉變成該類的字段後,變量引用的對象的生存週期被延長了。正常狀況下,在方法中最後一次使用參數/局部變量以後,這個參數/局部變量就會"離開操做做用域",結束其生命週期。可是,將變量轉變成另外一個類的字段後,只要包含字段的那個對象不"死",字段引用的對象也不會"死"。這在大多數應用程序中不是大的問題,但有時須要注意一下。
提示:毫無疑問,C#的lambda表達式功能很容易被開發人員濫用。我開始使用lambda表達式時,花了一些時間來熟悉它。畢竟,你在一個方法中寫的代碼實際再也不這個方法中,除了有違直覺,還使調試和但不執行變得更有挑戰性。
我給本身設定了一個規則:若是須要在回調放方法中包含3行以上代碼,就不適用lambda表達式。相反的,我會手動寫一個方法,併爲其分配一個本身的名稱。但若是使用得當,匿名方法趨勢能顯著提高開發人員效率和代碼的可維護性。在如下代碼中,使用Lambda表達式感受很是天然,沒有它們,這樣的代碼會很難寫、讀以及維護。
//建立並輸出和一個string數組 string[] names = {"green", "grant", "tom"}; //只獲取含有小寫字母’a’的名字 char charToFind = 'a'; names = Array.FindAll(names, c => c.IndexOf(charToFind) >= 0); //將每一個字符串的字符轉換爲大寫 names = Array.ConvertAll(names, c => c.ToUpper()); Array.ForEach(names,Console.WriteLine);
到本節爲止,使用委託都要求開發人員事先知道回調方法的原型。例如,若是fb是引用了一個Feedback委託的變量(第一節第二個示例程序),那麼爲了調用這個委託,代碼應該這樣寫:
fb(item); //item爲Int32類型
能夠看出,在編碼的時候,開發人員必須知道回調方法須要多少個參數,以及這些參數的具體類型。還好,開發人員幾乎老是知道這些信息,因此像前面那樣寫代碼是沒有問題的。
不過在個別狀況下,開發人員在編譯時並不知道這些信息。在"事件"討論EventSet類型時,曾經展現過一個例子。這個例子用一個字典來維護一組不一樣的委託類型。在運行時,爲了引起事件,要在字典中查找並調用委託。但在編譯時,咱們不能準確地知道要調用哪一個委託,哪些參數必須傳給委託的回調方法。
幸虧System.Delegate提供了一個CreateDelegate方法。在編譯時不知道委託的這些必要信息時,可利用這個方法來建立並調用一個委託。如下是MethodInfo未該方法定義的重載:
public abstract class MethodInfo:MethodBase { // 構造保證了一個靜態方法的委託 public virtual Delegate CreateDelegate(Type delegateType) { return null; } //構造保證了一個實例方法的委託;target引用this實參 public virtual Delegate CreateDelegate(Type delegateType,Object target) { return null; } }
建立好委託後,利用delegate的dynamicInvoke方法調用它,以下所示
// 調用委託並傳遞參數 public Object DynamicInvoke(params Object[] args);
使用反射API,首先必須獲取引用了回調方法的一個MethodInfo對象。而後,調用CreateDelegate方法來構造由第一個參數delegateType所標識的Delegate派生類型的對象。若是委託包裝了實例方法,還要向CreateDelegate傳遞一個target參數,指定做爲this參數傳給實例方法的對象。
全部CreateDelegate方法構造的都是從Delegate派生的一個類型新對象,具體類型由第一個參數type來標識。MethodInfo參數指出應該回調的方法;要用反射來獲取這個值。若是但願委託包裝一個實例方法,還要向CreateDelegate傳遞一個firstArgument參數,指定應做爲this參數(第一個參數)傳給實例方法的對象。最後,若是委託不能綁定到method參數指定的方法,CreateDelegate一般應該拋出一個異常。
System.Delegate的DynamicInvoke方法容許調用委託對象的回調方法,傳遞一組在運行時肯定的參數。調用DynamicInvoke時,它會在內部保證傳遞的參數與回調方法指望的參數兼容。若是兼容,就調用回調方法;不然拋出一個異常。DynamicInvoke返回回調方法所返回的對象。
下面代碼展現瞭如何使用CreateDelegate和DynamicInvoke方法:
// 下面是一些不一樣的委託定義 private delegate Object TwoInt32s(Int32 n1, Int32 n2); private delegate Object OneString(String s1); internal static class DelegateReflection { public static void Go(String[] args) { if (args.Length < 2) { String fileName = Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location); String usage = @"Usage:" + "{0}{1} delType methodName [Arg1] [Arg2]" + "{0} where delType must be TwoInt32s or OneString" + "{0} if delType is TwoInt32s, methodName must be Add or Subtract" + "{0} if delType is OneString, methodName must be NumChars or Reverse" + "{0}" + "{0}Examples:" + "{0} {1} TwoInt32s Add 123 321" + "{0} {1} TwoInt32s Subtract 123 321" + "{0} {1} OneString NumChars \"Hello there\"" + "{0} {1} OneString Reverse \"Hello there\""; Console.WriteLine(usage, Environment.NewLine, fileName); return; } // 將delType參數轉換爲一個委託類型 Type delType = Type.GetType(args[0]); if (delType == null) { Console.WriteLine("Invalid delType argument: " + args[0]); return; } Delegate d; try { // 將Arg1參數轉換爲一個方法 MethodInfo mi = typeof(Program).GetMethod(args[1], BindingFlags.NonPublic | BindingFlags.Static); // 建立包裝了靜態方法的一個委託對象 d = Delegate.CreateDelegate(delType, mi); } catch (ArgumentException) { Console.WriteLine("Invalid methodName argument: " + args[1]); return; } // 建立一個數組,其中只包含要經過委託對象傳給方法的參數 Object[] callbackArgs = new Object[args.Length - 2]; if (d.GetType() == typeof(TwoInt32s)) { try { // 將String類型的參數轉換爲Int32類型的參數 for (Int32 a = 2; a < args.Length; a++) callbackArgs[a - 2] = Int32.Parse(args[a]); } catch (FormatException) { Console.WriteLine("Parameters must be integers."); return; } } if (d.GetType() == typeof(OneString)) { // 只複製String參數 Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length); } try { // 調用委託並顯示結果 Object result = d.DynamicInvoke(callbackArgs); Console.WriteLine("Result = " + result); } catch (TargetParameterCountException) { Console.WriteLine("Incorrect number of parameters specified."); } } // 這個回調方法獲取2個Int32類型的參數 private static Object Add(Int32 n1, Int32 n2) { return n1 + n2; } // 這個回調方法獲取2個Int32類型的參數 private static Object Subtract(Int32 n1, Int32 n2) { return n1 - n2; } // 這個回調方法獲取1個String類型的參數 private static Object NumChars(String s1) { return s1.Length; } // 這個回調方法獲取1個String類型的參數 private static Object Reverse(String s1) { Char[] chars = s1.ToCharArray(); Array.Reverse(chars); return new String(chars); } }
運行結果
Usage: DelegateStudy2 delType methodName [Arg1] [Arg2] where delType must be TwoInt32s or OneString if delType is TwoInt32s, methodName must be Add or Subtract if delType is OneString, methodName must be NumChars or Reverse Examples: DelegateStudy2 TwoInt32s Add 123 321 DelegateStudy2 TwoInt32s Subtract 123 321 DelegateStudy2 OneString NumChars "Hello there" DelegateStudy2 OneString Reverse "Hello there" 請按任意鍵繼續. . .