匿名方法中的捕獲變數

来源: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
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...