前言:C#1中就已經有了委託的概念,可是其繁雜的用法並無引發開發者太多的關注,在C#2中,進行了一些編譯器上的優化,能夠用匿名方法來建立一個委託。同時,還支持的方法組和委託的轉換。順便的,C#2中增長了委託的協變和逆變。閉包
方法組這個詞的含義來自於方法的重載:咱們能夠定義一堆方法,這堆方法的名稱都同樣,可是接受的參數不一樣或者返回類型不一樣(總之就是簽名不一樣----除了名字),這就是方法的重載。ide
public static void SomeMethod(object helloworld) { Console.WriteLine(helloworld); } public static void SomeMethod() { Console.WriteLine("hello world"); }
ThreadStart ts = SomeMethod;
ParameterizedThreadStart ps = SomeMethod;學習
上面顯示的兩個調用沒有問題,編譯器可以找到與之匹配的相應方法去實例化相應的委託,可是,問題在於,對於自己已經重載成使用ThreadStart和ParameterizedThreadStart的Thread類來講(這裏是舉例,固然適用於全部這樣的狀況),傳入方法組會致使編譯器報錯:優化
Thread t=new Thread(SomeMethod); //編譯器報錯:方法調用具備二義性
一樣的狀況不能用於將一個方法組直接轉換成Delegate,須要顯式的去轉換:spa
Delegate parameterizedThreadStart = (ParameterizedThreadStart) SomeMethod; Delegate threadStart = (ThreadStart) SomeMethod;
C#1並不支持委託上面的協變性和逆變性,這意味着要爲每一個委託定義一個方法去匹配。C#2支持了委託的協變和逆變,這意味着咱們能夠寫下以下的代碼:線程
假定兩個類,其中一個繼承另外一個:設計
public class BaseClass { } public class DerivedClass : BaseClass { }
C#2支持以下寫法:code
class Program { delegate BaseClass FirstMethod(DerivedClass derivedClass); static void Main(string[] args) { FirstMethod firstMethod = SomeMethod; Console.ReadKey(); } static DerivedClass SomeMethod(BaseClass derivedClass) { return new DerivedClass(); } }
而在C#4中,支持了泛型類型和泛型委託的協變和逆變:orm
public class BaseClass{}對象
public class DerivedClass : BaseClass{}
Func<BaseClass, DerivedClass> firstFunc = delegate(BaseClass baseClass)
{ return new DerivedClass(); };
Func<DerivedClass, BaseClass> secondFunc = firstFunc;
本質上C#4泛型上的協變和逆變只是引用之間的轉換,並無在後面建立一個新的對象。
C#2支持了委託協變和逆變後會出現下面的問題:
假設如今BaseClass和DerivedClass改成下面這樣的:
public class BaseClass { public void CandidateAction(string x) { Console.WriteLine("Baseclass.CandidateAction"); } } public class DerivedClass : BaseClass { public void CandidateAction(object x) { Console.WriteLine("Derived.CandidateAction"); } }
在DerivedClass中重載了BaseClass中的方法,因爲C#2的泛型逆變和協變,寫下以下代碼:
class Program { delegate void FirstMethod(string x); static void Main(string[] args) { DerivedClass derivedClass=new DerivedClass(); FirstMethod firstMethod = derivedClass.CandidateAction; firstMethod("hello world");//DerivedClass.CandidateAction Console.ReadKey(); } }
輸出結果是」DerivedClass.CandidateAction!看到的這個結果確定是在C#2以及之後的結果,若是在C#1中,那麼該結果應該是輸出「BaseClass.CandidateAction"
下面這個出場的匿名方法是咱們以後學習linq和lambda等等一系列重要概念的始做俑者。
首先他要解決的問題是C#1中的委託調用起來太繁瑣的問題。在C#1中,要創建一個委託並使用這個委託的話一般要經歷四部,關鍵是無論你要調用一個多麼簡單的委託都要寫一個專門被委託調用的方法放到類裏面,若是沒有合適的類的話你還要新建一個類。。。
匿名方法是編譯器耍的小把戲,編譯器會在後臺建立一個類,來包含匿名方法所表示的那個方法,而後和普通委託調用同樣,通過那四部。CLR根本不知道匿名委託這個東西,就好像它不存在同樣。
若是不在意參數,能夠省略:delegate{...do something..},但涉及到方法重載時,要根據編譯器的提示補充相應的參數。
閉包。
delegate void MethodInvoker(); void EnclosingMethod() { int outerVariable = 5; //❶ 外部變量( 未捕獲的變量) string capturedVariable = "captured"; //❷ 被匿名方法捕獲的外部變量 if (DateTime. Now. Hour == 23) { int normalLocalVariable = DateTime. Now. Minute; //❸ 普通方法的局部變量 Console. WriteLine( normalLocalVariable); } MethodInvoker x = delegate() { string anonLocal = "local to anonymous method"; //❹ 匿名方法的局部變量 Console. WriteLine( capturedVariable + anonLocal); //❺ 捕獲外部變量 }; x(); }
被匿名方法捕捉到的確實是變量, 而不是建立委託實例時該變量的值。只有在委託被執行的時候纔會去採集這個被捕獲變量的值:
int a = 4; MethodInvoker invoker = delegate() { a = 5; Console.WriteLine(a); }; Console.WriteLine(a);//4 invoker();//5
要點在於,在整個方法中,咱們使用的是同一個被捕獲的變量。
簡單地說, 捕獲變量能簡化避免專門建立一些類來存儲一個委託須要處理的信息(除了做爲參數傳遞的信息以外)。
對於一個捕獲變量, 只要還有任何委託實例在引用它, 它就會一直存在。
delegate void MethodInvoker(); static MethodInvoker CreateMethodInvokerInstance() { int a = 4; MethodInvoker invoker = delegate () { Console.WriteLine(a); a++; }; invoker(); return invoker; }
static void Main(string[] args) { MethodInvoker invoker = CreateMethodInvokerInstance();//4 invoker();//5 invoker();//6 Console.ReadKey(); }
能夠看到,CreateDelegateInstance執行完成後,它對應的棧幀已經被銷燬,按道理說局部變量a也會隨之壽終正寢,可是後面仍是會繼續輸出5和6,緣由就在於,編譯器爲匿名方法建立的那個類捕獲了這個變量並保存它的值!CreateDelegateInstance擁有對該類的實例的一個引用,因此它能使用變量a,委託也有對該類的實例的一個引用,因此也能使用變量a。這個實例和其餘實例同樣都在堆上。
每當執行到聲明一個局部變量的做用域時, 就稱該局部變量被實例化 。
局部變量被聲明到棧上,因此在for這樣的結構中沒必要每次循環都實例化。
局部變量屢次被聲明和單次被聲明產生的效果是不同的。
delegate void MethodInvoker(); static void Main(string[] args) { List<MethodInvoker> methodInvokers=new List<MethodInvoker>(); for (int i = 0; i < 10; i++) { int count = i * 10; methodInvokers.Add(delegate() { Console.WriteLine(count); count++; }); } foreach (var item in methodInvokers) { item(); } methodInvokers[0]();//1 methodInvokers[0]();//2 methodInvokers[0]();//3 methodInvokers[1]();//11 Console.ReadKey(); }
上面的例子中,count在每次循環中都從新建立一次,致使委託捕獲到的變量都是新的、不同的變量,因此維護的值也不同。
若是把count去掉,換成這樣:
delegate void MethodInvoker(); static void Main(string[] args) { List<MethodInvoker> methodInvokers = new List<MethodInvoker>(); for (int i = 0; i < 10; i++) { methodInvokers.Add(delegate () { Console.WriteLine(i); i++; }); } foreach (var item in methodInvokers) { item(); } methodInvokers[0](); methodInvokers[0](); methodInvokers[0](); methodInvokers[1](); Console.ReadKey(); }
此次委託直接捕獲的是i這個變量,for循環中的循環變量被認爲是聲明在for循環外部的一個變量,相似於下面的代碼:
int i=0; for(i;i<10;i++) { ..... }
注意,這個例子能夠用局部變量只被實例化一次仍是屢次的道理說服,背後的原理是編譯器建立的那個類實例化的地方不同。第一次用count變量來接受i的值時,在for循環的內部每循環一次編譯器都會建立一個新的實例來保存count的值並被委託調用,而把count去掉時,編譯器建立的這個類會在for循環外部被建立,因此只會建立一次,捕獲的時i的最終的那個值。因此,我猜測,編譯器建立的那個類和被捕獲的變量的做用域時有關係的,編譯器建立的那個類的實例化的位置應該和被捕獲的變量的實例化的位置或者說是做用域相同。
看下面的例子:
delegate void MethodInvoker(); static void Main(string[] args) { MethodInvoker[] methods=new MethodInvoker[2]; int outSide = 1; for (int i = 0; i < 2; i++) { int inside = 1; methods[i] = delegate() { Console.WriteLine($"outside:{outSide}inside:{inside}"); outSide++; inside++; }; } MethodInvoker first = methods[0]; MethodInvoker second = methods[1]; first(); first(); first(); second(); second(); Console.ReadKey(); }
這張圖說明了上面的問題。
使用捕獲變量時, 請參照如下規則。
[英]Jon Skeet. 深刻理解C#(第3版) (圖靈程序設計叢書) (Kindle 位置 4363-4375). 人民郵電出版社. Kindle 版本.