C#複習筆記(3)--C#2:解決C#1的問題(進入快速通道的委託)

委託

前言: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(); }

這張圖說明了上面的問題。

使用捕獲變量時, 請參照如下規則。

  • 若是用或不用捕獲變量時的代碼一樣簡單, 那就不要用。
  • 捕獲由for或foreach語句聲明的變量以前, 思考你的委託是否須要在循環迭代結束以後延續, 以及是否想讓它看到那個變量的後續值。 若是須要, 就在循環內另建一個變量, 用來複制你想要的值。( 在 C# 5 中, 你 沒必要 擔憂 foreach 語句, 但 仍需 當心 for 語句。) 若是建立多個委託實例(不論是在循環內, 仍是顯式地建立), 並且捕獲了變量, 思考一下是否 但願它們捕捉同一個變量。
  • 若是捕捉的變量不會發生改變( 不論是在匿名方法中, 仍是在包圍着匿名方法的外層方法主體中), 就不須要有這麼多擔憂。
  • 若是你建立的委託實例永遠不從方法中「 逃脫」, 換言之, 它們永遠不會存儲到別的地方, 不會返回, 也不會用於啓動線程—— 那麼事情就會簡單得多。
  • 從垃圾回收的角度, 思考任 捕獲變量被延長的生存期。 這方面的問題通常都不大, 但假如捕獲的對象會產生昂貴的內存開銷, 問題就會凸現出來。

[英]Jon Skeet. 深刻理解C#(第3版) (圖靈程序設計叢書) (Kindle 位置 4363-4375). 人民郵電出版社. Kindle 版本.

 本章劃重點

  • 捕獲的是變量, 而不是建立委託實例時它的值。
  • 捕獲的變量的生存期被延長了, 至少和捕捉它的委託同樣 長。
  • 多個委託能夠捕獲同一個變量……
  • …… 但在循環內部, 同一個變量聲明實際上會引用不一樣的變量「 實例」。
  • 在for循環的聲明中建立的變量僅在循環持續期間有效—— 不會在每次循環迭代時都實例化。 這一狀況對 C# 5以前的foreach語句也適用。
  • 必要時建立額外的類型來保存捕獲變量。 要當心! 簡單幾乎老是比耍小聰明好。
相關文章
相關標籤/搜索