匿名方法中的捕獲變數

来源:https://www.cnblogs.com/az4215/archive/2019/10/07/11629433.html

乍一接觸"匿名方法中的捕獲變數"這一術語可能會優點蒙,那什麼是"匿名方法中的捕獲變數"呢?在章節未開始之前,我們先定義一個委托:public delegate void MethodInvoke(); 1、閉包和不同類型的變數: 首先,大家應該都知道"閉包",它的概念是:一個函數除了能通過提供給它的 ...


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

1、閉包和不同類型的變數:

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

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

  2)捕獲的外部變數(captured outer variable)通常簡稱捕獲變數(captured variable),它是在匿名方法內部使用的外部變數。

  這些定義看起來雲里霧裡的,那接下來以一個例子來說明: 

 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 }

2、捕獲變數的行為:

  如果你運行了上述代碼,你會發現匿名方法捕捉到的確實是變數,而不是創建委托實例時該變數的值。通俗的說就是只有在匿名方法被調用時才會被使用。 

 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()

3、捕獲變數到底有什麼用處:

  捕獲變數能簡化避免專門創建一些類來存儲一個委托需要處理的信息。

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

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

4、捕獲變數的延長生存期:

  到目前為止,我麽一直在創建委托實例的方法內部使用委托實例。在這種情況下,你對捕獲變數的生存期(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。另外,委托也對該實例的一個引用,這個實例和其他實例一樣都在堆上。除非委托準備好垃圾回收,否則那個實例是不會被回收的。

5、局部變數實例化:

  下麵將展示一個例子。

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變數是在迴圈內部聲明的,所以每次迴圈迭代,它都會被實例化。這樣一來,每個委托捕捉到的都是不同的變數。

6、共用和非共用的變數混合使用:

 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變數。

7、總結:

  如何合理使用捕獲變數?

    1)如果用或不用捕獲變數的代碼同樣簡單,那就不要用。

    2)捕獲由for或foreach語句聲明的變數之前,思考你的委托是否需要再迴圈迭代結束之後延續,以及是否想讓它看到那個變數的後續值。如果不是,就在迴圈內另建一個變數,用來複制你想要的值。

    3)如果創建多個委托實例,而且捕獲了變數,思考下是否希望它們捕獲同一變數。

    4)如果捕獲的變數不會發生變化,就不需要擔心。

    5)如果你創建的委托實例永遠不會存儲別的地方,不會返回,也不會啟動線程。

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

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


您的分享是我們最大的動力!

更多相關文章
  • 本文源碼: "GitHub·點這裡" || "GitEE·點這裡" 一、ClickHouse簡介 1、基礎簡介 Yandex開源的數據分析的資料庫,名字叫做ClickHouse,適合流式或批次入庫的時序數據。ClickHouse不應該被用作通用資料庫,而是作為超高性能的海量數據快速查詢的分散式實時處 ...
  • 非常興奮加入博客園大家庭 ...
  • 經典例題 if嵌套 1.用戶輸入賬號2.用戶輸入密碼3.判斷用戶的賬號是不是alex4.如果賬號是alex在繼續判斷密碼是不是alexdsb5.賬號和密碼都正確提示用戶alex就是一個dsb6.如果賬號正確密碼錯誤提示密碼錯誤7.如果賬號錯誤提示賬號錯誤 user = input("請輸入賬號:") ...
  • 本文主要實現無動態刷新查詢後臺數據功能,主要用到ajax+ashx+sqlserver進行交互. 首先需要引用Jquery: html腳本: 前臺通過一個事件來調用ashx: 後臺來接收前臺傳過來的值,對其進行操作: SerializerHelper類的定義: 如果向後臺傳入多個參數在data裡面用 ...
  • 前景:要操作的數據表必須添加主鍵(方式:進入資料庫-->數據表名-->設計-->列名右鍵-->設置主鍵) 可在伺服器資源管理器中查看是否設置了主鍵(主鍵會有一把鑰匙的圖樣) 1)、項目名右鍵-->新建項-->ADO.NET數據模型 選擇第一個“來自資料庫的EF設計器”就行 如果是第一次連接,點擊新建 ...
  • .Net Core應用發佈到IIS主要是如下的三個步驟: (1)在Windows Server上安裝 .Net Core Hosting Bundle (2)在IIS管理器中創建IIS站點 (3)部署ASP.NET Core應用 一.安裝 .Net Core Hosting Bundle 打開鏈接 ...
  • 一般情況下,一個 .NET 程式集載入到程式中以後,它的類型信息以及原生代碼等數據會一直保留在記憶體中,.NET 運行時無法回收它們,如果我們要實現插件熱載入 (例如 Razor 或 Aspx 模版的熱更新) 則會造成記憶體泄漏。在以往,我們可以使用 .NET Framework 的 AppDomain ...
  • 很多的.NET開發者在接觸.Net Core之前,對於linux系統一點也不瞭解,也未曾有過主動去學習的念頭。在接觸了.Net Core之後才會慢慢學習linux相關知識,很多同學想轉Java,這個很扎心,你有很好的條件轉向.NET Core為啥要轉Java,據說目前市場上Java多如牛毛,兩年以內 ...
一周排行
  • 該方式是直接對屏幕進行截圖操作UserControl chartContainPanel = new UserControl();Graphics graph = chartContainPanel.CreateGraphics();Size s = chartContainPanel.Size;B... ...
  • dotnetcore3.1 WPF 中使用依賴註入 Intro 在 ASP.NET Core 中預設就已經集成了依賴註入,最近把 "DbTool" 遷移到了 WPF dotnetcore 3.1, 在 WPF 中我們也希望能夠使用依賴註入,下麵來介紹一下如何在 WPF dotnetcore3.1 中 ...
  • 原來的C 程式都有Main的,現在用vs新建一個Wpf項目,啟動似乎變成App.xmal,前期項目中為了獲取啟動參數,很是折騰了一番: 1.先是修改App.xaml,添加StartUp事件 2.然後編輯Application_Startup,判斷e.Args數組 總感覺跟又臭又長的裹腳布一樣,不爽。 ...
  • 冒泡排序原理:(升序)通過當前位置數和後一個位置數進行比較 如果當前數比後一個數大 則交換位置, 完成後 比較基數的位置變成下一個數。直到數組末尾,當程式運行完第一遍 最大的數已經排序到最後一個位置了。次數可以減少迴圈數不用管最後一個數 降序排序同理 不過是把比較方式變成判斷當前數是否小於下一個數 ...
  • 一、前言 這方面的資料很多,重覆的寫沒必要,但是最近一直在學習身份驗證和授權相關東東,為了成體系還是寫一篇,主要是從概念上理解identity系統。 參考:https://www.cnblogs.com/r01cn/p/5179506.html 二、概述 幾乎所有系統都包含用戶、角色、許可權、登錄、註 ...
  • 首先我們使用最簡單的模板案例,裡面有一個Counter計數器,你可以在創建模板中找到。 首先需要設置運行調試方式為IIS Express。這意味著,MAC可能不能使用調試。 然後開啟運行而不調試(Ctrl+F5) 按Shift + Alt + D,會出現一個新的頁面。 如果你想用Chrome調試,復 ...
  • 實體映射時,遇到複雜類型,可選擇下述方法處理: NotMapped,跳過映射 在複雜類型上聲明 [Owned],但僅限該複雜類型是全部由簡單值類型組成的 自定義序列化方法 示例: IPInfo使用了owned,對IPEndPoint使用自定義序列化,對VersionInfo使用JSON序列化 @@@... ...
  • .NET Core 3 Web Api Cors fetch 一直 307 Temporary Redirect 繼上一篇 ".net core 3 web api jwt 一直 401" 為添加 所述的坑後, 本次為添加 ,又踩坑了。 自從 .NET Core 2.2 之後,CORS跨域配置代碼發 ...
  • 在前一章已經學習過WPF動畫的第一條規則——每個動畫依賴於一個依賴項屬性。然而,還有另一個限制。為了實現屬性的動態化(換句話說,使用基於時間的方式改變屬性的值),需要有支持相應數據類型的動畫類。例如,Button.Width屬性使用雙精度數據類型。為實現屬性的動態化,需要使用DoubleAnimat ...
  • WPF dotnet core 3.1 基於 `Microsoft.Extensions.Localization` 實現基本的多語言支持 ...
x