標籤:語句 date 捕獲變數 之間 c# 建立 聲明 泛型類 star
委託
前言:C#1中就已經有了委託的概念,但是其繁雜的用法並沒有引起開發人員太多的關注,在C#2中,進行了一些編譯器上的最佳化,可以用匿名方法來建立一個委託。同時,還支援的方法組和委託的轉換。順便的,C#2中增加了委託的協變和逆變。
方法群組轉換
方法組這個詞的含義來自於方法的重載:我們可以定義一堆方法,這堆方法的名稱都一樣,但是接受的參數不同或者傳回型別不同(總之就是簽名不同----除了名字),這就是方法的重載。
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,需要顯式的去轉換:
Delegate parameterizedThreadStart = (ParameterizedThreadStart) SomeMethod;Delegate threadStart = (ThreadStart) SomeMethod;
協變性和逆變性
C#1並不支援委託上面的協變性和逆變性,這意味著要為每個委託定義一個方法去匹配。C#2支援了委託的協變和逆變,這意味著我們可以寫下如下的代碼:
假定兩個類,其中一個繼承另一個:
public class BaseClass { } public class DerivedClass : BaseClass { }
C#2支援如下寫法:
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中,支援了泛型型別和泛型委派的協變和逆變:
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(); }
這張圖說明了上面的問題。
使用捕獲變數時, 請參照以下規則。
- 如果用或不用捕獲變數時的代碼同樣簡單, 那就不要用。
- 捕獲由for或foreach語句聲明的變數之前, 思考你的委託是否需要在迴圈迭代結束之後延續, 以及是否想讓它看到那個變數的後續值。 如果需要, 就在迴圈內另建一個變數, 用來複製你想要的值。( 在 C# 5 中, 你 不必 擔心 foreach 語句, 但 仍需 小心 for 語句。) 如果建立多個委託執行個體(不管是在迴圈內, 還是顯式地建立), 而且捕獲了變數, 思考一下是否 希望它們捕捉同一個變數。
- 如果捕捉的變數不會發生改變( 不管是在匿名方法中, 還是在包圍著匿名方法的外層方法主體中), 就不需要有這麼多擔心。
- 如果你建立的委託執行個體永遠不從方法中“ 逃脫”, 換言之, 它們永遠不會儲存到別的地方, 不會返回, 也不會用於啟動線程—— 那麼事情就會簡單得多。
- 從記憶體回收的角度, 思考任 捕獲變數被延長的生存期。 這方面的問題一般都不大, 但假如捕獲的對象會產生昂貴的記憶體開銷, 問題就會凸現出來。
[英]Jon Skeet. 深入理解C#(第3版) (圖靈程式設計叢書) (Kindle 位置 4363-4375). 人民郵電出版社. Kindle 版本.
本章劃重點
- 捕獲的是變數, 而不是建立委託執行個體時它的值。
- 捕獲的變數的生存期被延長了, 至少和捕捉它的委託一樣 長。
- 多個委託可以捕獲同一個變數……
- …… 但在迴圈內部, 同一個變數聲明實際上會引用不同的變數“ 執行個體”。
- 在for迴圈的聲明中建立的變數僅在迴圈持續期間有效—— 不會在每次迴圈迭代時都執行個體化。 這一情況對 C# 5之前的foreach語句也適用。
- 必要時建立額外的類型來儲存捕獲變數。 要小心! 簡單幾乎總是比耍小聰明好。
C#複習筆記(3)--C#2:解決C#1的問題(進入快速通道的委託)