匿名方法中的捕獲變數

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

乍一接觸"匿名方法中的捕獲變數"這一術語可能會優點蒙,那什麼是"匿名方法中的捕獲變數"呢?在章節未開始之前,我們先定義一個委托: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#_第三版 


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

-Advertisement-
Play Games
更多相關文章
  • 本文源碼: "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多如牛毛,兩年以內 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...