匿名方法中的捕獲變量

  乍一接觸"匿名方法中的捕獲變量"這一術語可能會優勢蒙,那什麼是"匿名方法中的捕獲變量"呢?在章節未開始以前,咱們先定義一個委託:public delegate void MethodInvoke();閉包

一、閉包和不一樣類型的變量:ide

  首先,你們應該都知道"閉包",它的概念是:一個函數除了能經過提供給它的參數交互以外,還能同環境進行更大程度的互動。但這個定義過於抽象,還須要理解兩個術語:函數

  1)外部變量(outer variable)指做用域內包括匿名方法的局部變量或參數(不包括ref和out參數)。在類的實例成員內部的匿名方法中,this引用也被認爲是一個外部變量。this

  2)捕獲的外部變量(captured outer variable)一般簡稱捕獲變量(captured variable),它是在匿名方法內部使用的外部變量。編碼

  這些定義看起來雲裏霧裏的,那接下來以一個例子來講明: spa

 1 public void EnClosingMethod()  2 {  3     int outerVariable = 5; // 外部變量
 4     string captureVariable = "captured"; // 被匿名方法捕獲的外部變量
 5     if (DateTime.Now.Hour == 23)  6  {  7         int normalLocalVariable = DateTime.Now.Minute; // 普通方法的局部變量
 8  Console.WriteLine(normalLocalVariable);  9  } 10     MethodInvoke x = delegate () 11  { 12         string anonLocal = "local to anonymous method"; // 匿名方法的局部變量
13         Console.WriteLine(captureVariable + anonLocal); // 捕獲外部變量captureVariable
14  }; 15  Console.WriteLine(outerVariable); 16  x(); 17 }

二、捕獲變量的行爲:線程

  若是你運行了上述代碼,你會發現匿名方法捕捉到的確實是變量,而不是建立委託實例時該變量的值。通俗的說就是隻有在匿名方法被調用時纔會被使用。 debug

 1 string captured = "before x is created";  2 MethodInvoke x = delegate
 3 {  4  Console.WriteLine(captured);  5     captured = "change by x";  6 };  7 captured = "directly before x is invoked";  8 x();  9 Console.WriteLine(captured); 10 captured = "before second invocation"; 11 x();

  上述代碼的執行順序是這樣子的(能夠debug):定義變量captured => 聲明匿名方法MethodInvoke x => 將captured的值修改成"directly before x is invoked" => 緊接着調用委託x(),這個時候會進入匿名方法 => 首先輸出captured的值"directly before x is invoked",而後修改成"change by x" => 匿名方法調用結束,來到第9行,輸出captured的值"change by x" => 第10行從新給captured賦值"before second invocation" => 調用x()設計

三、捕獲變量到底有什麼用處:code

  捕獲變量能簡化避免專門建立一些類來存儲一個委託須要處理的信息。

1 List<People> FindAllYoungerThan(List<People> people, int limit) 2 { 3     return people.Where(person => person.Age < limit).ToList(); 4 }

  咱們在委託實例內部捕獲了limit參數——若是僅有匿名方法而沒有捕獲變量,就只能在匿名方法中使用一個"硬編碼"的限制年齡,而不能使用做爲參數傳遞的limit。這樣的設計可以準備描述咱們的"目的",而不是將大量的精力放在"過程"上。

四、捕獲變量的延長生存期:

  到目前爲止,我麼一直在建立委託實例的方法內部使用委託實例。在這種狀況下,你對捕獲變量的生存期(lifetime)不會又太大的疑問。可是,假如委託實例"逃"到另外一個黑暗的世界(big bad world),那會發生什麼?假如建立它的那個方法結束了,它將何以應對?

  在理解這種問題時,最簡單的辦法就是指定一個規則,給出一個例子,而後思考假如沒有那個規則,會發生什麼:對於一個捕獲變量,只要還有任何委託實例在引用它,它就會一直存在。

 1 private static void Main(string[] args)  2 {  3     MethodInvoke x = CreateDelegateInstance();  4  x();  5  x();  6 }  7 
 8 private static MethodInvoke CreateDelegateInstance()  9 { 10     int counter = 5; 11 
12     MethodInvoke ret = delegate
13  { 14  Console.WriteLine(counter); 15         counter++; 16  }; 17 
18  ret(); 19     return ret; 20 }

  輸出的結果:

  咱們通常認爲counter在棧上,因此只要與CreateDelegateInstance對應的棧幀被銷燬,counter隨之消失,可是從結果來看,顯然咱們的認知是有問題的。事實上,編譯器建立了一個額外的類容納變量。CreateDelegateInstance方法擁有對該類的一個實例的引用,因此它能使用counter。另外,委託也對該實例的一個引用,這個實例和其餘實例同樣都在堆上。除非委託準備好垃圾回收,不然那個實例是不會被回收的。

五、局部變量實例化:

  下面將展現一個例子。

1 int single; 2 for (int i = 0; i < 10; i++) 3 { 4     single = 5; 5     Console.WriteLine(single + i); 6 }
1 for (int i = 0; i < 10; i++) 2 { 3     int multiple = 5; 4     Console.WriteLine(multiple + i); 5 }

  上述兩段代碼在語義和功能上是同樣的,但在內存開銷上顯然第一種寫法比第二種佔用較小的內存。single變量只實例化一次,而multiple變量將實例化10次。當一個變量被捕獲時,捕捉的是變量的"實例"。若是在循環內捕捉multiple,第一次循環迭代時捕獲的變量與第二次循環時捕獲的變量是不一樣的。

 1 List<MethodInvoke> list = new List<MethodInvoke>();  2 for (int index = 0; index < 5; index++)  3 {  4     int counter = index * 10;  5     list.Add(delegate
 6  {  7  Console.WriteLine(counter);  8         counter++;  9  }); 10 } 11 foreach (MethodInvoke t in list) 12 { 13  t(); 14 } 15 
16 list[0](); 17 list[0](); 18 list[0](); 19 
20 list[1]();

  輸出結果:

  上述代碼首先建立了5個不一樣的委託實例,調用委託時,會先打印counter值,再對它進行遞增。因爲counter變量是在循環內部聲明的,因此每次循環迭代,它都會被實例化。這樣一來,每一個委託捕捉到的都是不一樣的變量。

六、共享和非共享的變量混合使用:

 1 MethodInvoke[] delegates = new MethodInvoke[2];  2 int outside = 0;  3 
 4 for (int i = 0; i < 2; i++)  5 {  6     int inside = 0;  7     delegates[i] = delegate
 8  {  9         Console.WriteLine($"{outside},{inside}"); 10         outside++; 11         inside++; 12  }; 13 } 14 
15 MethodInvoke first = delegates[0]; 16 MethodInvoke second = delegates[1]; 17 
18 first(); 19 first(); 20 first(); 21 
22 second(); 23 second();

  輸出結果:

  首先outside變量只聲明瞭一次,但inside變量每次循環迭代,都會實例化一個新的inside變量。這意味着當咱們建立委託實例時,outside變量將由委託實例共享,但每一個委託實例都有它們本身的inside變量。

七、總結:

  如何合理使用捕獲變量?

    1)若是用或不用捕獲變量的代碼一樣簡單,那就不要用。

    2)捕獲由for或foreach語句聲明的變量以前,思考你的委託是否須要再循環迭代結束以後延續,以及是否想讓它看到那個變量的後續值。若是不是,就在循環內另建一個變量,用來複制你想要的值。

    3)若是建立多個委託實例,並且捕獲了變量,思考下是否但願它們捕獲同一變量。

    4)若是捕獲的變量不會發生變化,就不須要擔憂。

    5)若是你建立的委託實例永遠不會存儲別的地方,不會返回,也不會啓動線程。

    6)從垃圾回收的角度,思考任何捕獲變量被延長的生存期。這個問題通常都不大,但假如捕獲的對象會產生昂貴的內存開銷,問題就會凸顯出來。

參考:深刻理解C#_第三版 

相關文章
相關標籤/搜索