匿名方法中的捕獲變數

来源: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#_第三版 


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

更多相關文章
  • 一、前言 在實際項目開發過程中,我們使用到的各種 ORM 組件都可以很便捷的將我們獲取到的數據綁定到對應的 List<T> 集合中,因為我們最終想要在頁面上展示的數據與資料庫實體類之間可能存在很大的差異,所以這裡更常見的方法是去創建一些對應於頁面數據展示的 `視圖模型` 類,通過對獲取到的數據進行二 ...
  • 在GitHub上有個項目,本來是作為自己研究學習.net core的Demo,沒想到很多同學在看,還給了很多星,所以覺得應該升成3.0,整理一下,寫成博分享給學習.net core的同學們。 項目名稱:Asp.NetCoreExperiment 項目地址:https://github.c... ...
  • 本文轉自:https://www.cnblogs.com/shangwater/p/5449470.html 錯誤:程式集綁定日誌記錄被關閉。 解決方法: 將IIS中的應用程式服務池中"啟動32位應用程式"設置為true。 錯誤描述: 部署到IIS中的應用程式出現如下錯誤(Win7 x64位 .ne ...
  • 一、簡要說明 ABP vNext 當中的審計模塊早在 "依賴註入與攔截器" 一文中有所提及,但沒有詳細的對其進行分析。 審計模塊是 ABP vNext 框架的一個基本組件,它能夠提供一些實用日誌記錄。不過這裡的日誌不是說系統日誌,而是說介面每次調用之後的執行情況(執行時間、傳入參數、異常信息、請求 ...
  • 1.ServiceStack.Redis封裝 封裝的Redis操作類名為RedisHandle,如下代碼塊(只展示部分代碼),它的特點: 1)使用連接池管理連接,見代碼中的PooledClientManager屬性。如果不用連接池,而是代碼直接RedisClient client = new Red ...
一周排行
  • 一、背景 代碼實例:https://gitee.com/D_C_L/CurtainEtcAOP.git我們實際系統中有很多操作,是不管做多少次,都應該產生一樣的效果或返回一樣的結果。 例如: 1. 前端重覆提交選中的數據,應該後臺只產生對應這個數據的一個反應結果。 2. 我們發起一筆付款請求,應該只 ...
  • 關鍵字:流程未來節點處理人 工作流快速開發平臺 工作流流設計 業務流程管理 asp.net 開源工作流 業務背景:一個流程在啟動起來後,是可以對一些節點計算出來處理人是誰,流程的走向。對於另外一些節點處理人有可能需要相關的人員調整的。在一些審批的環境下,需要把能夠計算出來的節點處理人在發起時計算出來... ...
  • 簡述 我們做軟體工作的雖然每天都離不開網路,可網路協議細節卻不是每個人都會接觸和深入瞭解。我今天就來和大家一起學習下Socket,並寫一個簡單的聊天程式。 一些基礎類 首先我們每天打開瀏覽器訪問網頁信息都是使用的HTTP/HTTPS協議,而HTTP是通過的TCP建立的連接。TCP底層又是通過的Soc ...
  • 點這裡進入ABP進階教程目錄 在功能按鈕區增加一個自定義按鈕 - Add(創建課程) 添加按鈕 打開展示層(即JD.CRS.Web.Mvc)的\wwwroot\view-resources\Views\Course\Index.js //用以存放Course查詢相關腳本 自帶按鈕已有五個我們再添加一 ...
  • 點這裡進入ABP進階教程目錄 我們嘗試在新增/編輯界面增加一個下拉框用來代替輸入框編輯Status 添加實體 打開領域層(即JD.CRS.Core)的Entitys目錄 //用以存放實體對象添加一個類StatusCode.cs //狀態信息 更新模型 更新查詢視圖模型 打開展示層(即JD.CRS.W ...
  • 在項目視圖中,找到-》輸出 視窗,在視窗中選擇ASP.NET Core Web伺服器,調試項目即可看到執行的sql語句 ...
  • 前言: 通過Fiddler抓取瀏覽器請求數據,相信大家已經都會用了,我們知道Fiddler是通過在本機計算器添加一個預設的代理伺服器來實現的抓包數據的,埠號為:8888。 其實當我們打開Fiddler的設置也可以看到: 然後查看本地計算器的網路代理設置: 基於上面的原理,Fiddler就實現了經過 ...
  • 場景 Winform控制項-DevExpress18下載安裝註冊以及在VS中使用: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/100061243 在上面已經實現DevExpress的安裝之後,拖拽一個TreeList,然後怎樣給 ...
  • 場景 Winform控制項-DevExpress18下載安裝註冊以及在VS中使用: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/100061243 DevExpress的TreeList怎樣設置數據源,從實例入手: https:/ ...
  • 場景 在開發中,經常會有一些全局作用域的常量、欄位、屬性、方法等。 需要將這些設置為全局作用域保存且其實例唯一。 註: 博客主頁: https://blog.csdn.net/badao_liumang_qizhi 關註公眾號 霸道的程式猿 獲取編程相關電子書、教程推送與免費下載。 實現 首先新建一 ...
x