理解C#中的閉包

来源:https://www.cnblogs.com/blurhkh/archive/2018/08/25/9535289.html
-Advertisement-
Play Games

1、 閉包的含義 首先閉包並不是針對某一特定語言的概念,而是一個通用的概念。除了在各個支持函數式編程的語言中,我們會接觸到它。一些不支持函數式編程的語言中也能支持閉包(如java8之前的匿名內部類)。 在看過的對於閉包的定義中,個人覺得比較清晰的是在《JavaScript高級程式設計》這本書中看到的 ...


1、 閉包的含義

首先閉包並不是針對某一特定語言的概念,而是一個通用的概念。除了在各個支持函數式編程的語言中,我們會接觸到它。一些不支持函數式編程的語言中也能支持閉包(如java8之前的匿名內部類)。

在看過的對於閉包的定義中,個人覺得比較清晰的是在《JavaScript高級程式設計》這本書中看到的。具體定義如下:

閉包是指有權訪問另一個函數作用域中的變數的函數

註意,閉包這個詞本身指的是一種函數。而創建這種特殊函數的一種常見方式是在一個函數中創建另一個函數。

2、 在C# 中使用閉包(例子選取自《C#函數式程式設計》)

下麵我們通過一個簡單的例子來理解C#閉包

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(GetClosureFunction()(30));
    }

    static Func<int, int> GetClosureFunction()
    {
        int val = 10;
        Func<int, int> internalAdd = x => x + val;

        Console.WriteLine(internalAdd(10));

        val = 30;
        Console.WriteLine(internalAdd(10));

        return internalAdd;
    }
}

上述代碼的執行流程是Main函數調用GetClosureFunction函數,GetClosureFunction返回了委托internalAdd並被立即執行了。

輸出結果依次為20、40、60

對應到一開始提出的閉包的概念。這個委托internalAdd就是一個閉包,引用了外部函數GetClosureFunction作用域中的變數val。

註意:internalAdd有沒有被當做返回值和閉包的定義無關。就算它沒有被返回到外部,它依舊是個閉包。

3、 理解閉包的實現原理

我們來分析一下這段代碼的執行過程。在一開始,函數GetClosureFunction內定義了一個局部變數val和一個利用lamdba語法糖創建的委托internalAdd。

第一次執行委托internalAdd 10 + 10 輸出20

接著改變了被internalAdd引用的局部變數值val,再次以相同的參數執行委托,輸出40。顯然局部變數的改變影響到了委托的執行結果。

GetClosureFunction將internalAdd返回至外部,以30作為參數,去執行得到的結果是60,和val局部變數最後的值30是一致的。

val 作為一個局部變數。它的生命周期本應該在GetClosureFunction執行完畢後就結束了。為什麼還會對之後的結果產生影響呢?

我們可以通過反編譯來看下編譯器為我們做的事情。

為了增加可讀性,下麵的代碼對編譯器生成的名字進行修改,並對代碼進行了適當的整理。

class Program
{
    sealed class DisplayClass
    {
        public int val;

        public int AnonymousFunction(int x)
        {
            return x + this.val;
        }
    }

    static void Main(string[] args)
    {
        Console.WriteLine(GetClosureFunction()(30));
    }

    static Func<int, int> GetClosureFunction()
    {
        DisplayClass displayClass = new DisplayClass();
        displayClass.val = 10;
        Func<int, int> internalAdd = displayClass.AnonymousFunction;

        Console.WriteLine(internalAdd(10));

        displayClass.val = 30;
        Console.WriteLine(internalAdd(10));

        return internalAdd;
    }
}

編譯器創建了一個匿名類(如果不需要創建閉包,匿名函數只會是與GetClosureFunction生存在同一個類中,並且委托實例會被緩存,參見clr via C# 第四版362頁),併在GetClosureFunction中創建了它實例。局部變數實際上是作為匿名類中的欄位存在的。

4、 C#7對於不作為返回值的閉包的優化

如果在vs2017中編寫第二節的代碼。會得到一個提示,詢問是否把lambda表達式(匿名函數)托轉為本地函數。本地函數是c#7提供的一個新語法。那麼使用本地函數實現閉包又會有什麼區別呢?

如果還是第二節那樣的代碼,改成本地函數,查看IL代碼。實際上不會發生任何變化。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(GetClosureFunction()(30));
    }

    static Func<int, int> GetClosureFunction()
    {
        int val = 10;
        int InternalAdd(int x) => x + val;

        Console.WriteLine(InternalAdd(10));

        val = 30;
        Console.WriteLine(InternalAdd(10));

        return InternalAdd;
    }
}

但是當internalAdd不需要被返回時,結果就不一樣了。

下麵分別來看下匿名函數和本地函數創建不作為返回值的閉包的時候演示代碼及經整理的反編譯代碼。

匿名函數

static void GetClosureFunction()
{
    int val = 10;
    Func<int, int> internalAdd = x => x + val;

    Console.WriteLine(internalAdd(10));

    val = 30;
    Console.WriteLine(internalAdd(10));
}

經整理的反編譯代碼

sealed class DisplayClass
{
    public int val;

    public int AnonymousFunction(int x)
    {
        return x + this.val;
    }
}

static void GetClosureFunction()
{
    DisplayClass displayClass = new DisplayClass();
    displayClass.val = 10;
    Func<int, int> internalAdd = displayClass.AnonymousFunction;

    Console.WriteLine(internalAdd(10));

    displayClass.val = 30;
    Console.WriteLine(internalAdd(10));
}

本地函數

class Program
{
    static void Main(string[] args)
    {
    }

    static void GetClosureFunction()
    {
        int val = 10;
        int InternalAdd(int x) => x + val;

        Console.WriteLine(InternalAdd(10));

        val = 30;
        Console.WriteLine(InternalAdd(10));
    }
}

經整理的反編譯代碼

// 變化點1:由原來的class改為了struct
struct DisplayClass
{
    public int val;

    public int AnonymousFunction(int x)
    {
        return x + this.val;
    }
}

static void GetClosureFunction()
{
    DisplayClass displayClass = new DisplayClass();
    displayClass.val = 10;

    // 變化點2:不再構建委托實例,直接調用值類型的實例方法
    Console.WriteLine(displayClass.AnonymousFunction(10));

    displayClass.val = 30;
    Console.WriteLine(displayClass.AnonymousFunction(10));
}

上述這兩點變化在一定程度上能夠帶來性能的提升,所以在官方的推薦中,如果委托的使用不是必要的,更推薦使用本地函數而非匿名函數。

如果本博客描述的內容存在問題,希望大家能夠提出寶貴的意見。堅持寫博客,從這一篇開始。


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

-Advertisement-
Play Games
更多相關文章
  • 一、三元表達式的使用 name = 'alex' age = 20 if name == 'alex' else 22 print(age) 二、列表推導式(聲明式編程) l = ['alex%s' %i for i in range(10) if i > 5] print(l) 三、生成器表達式 ...
  • 前言 Python 是一種極具可讀性和通用性的編程語言。Python 這個名字的靈感來自於英國喜劇團體 Monty Python,它的開發團隊有一個重要的基礎目標,就是使語言使用起來很有趣。Python 易於設置,並且是用相對直接的風格來編寫,對錯誤會提供即時反饋,對初學者而言是個很好的選擇。 Py ...
  • 我們在coding的時候,會經常遇到要求我們處理InterruptedException的情況,本文將解釋如何正確處理此異常以及背後的原因。 ...
  • 相信很多微信用戶在使用微信給朋友,同事發送相冊中的文件時,微信會顯示你手機中的視頻文件,這樣很不方便. 如果要完全不顯示視頻文件: 隨便在手機中建立一個文件夾,名字叫 ".nomedia",把視頻文件丟進去就行了. 如何隱藏你希望隱藏的視頻文件: 隨便在手機中建立一個文件夾,名字叫 ".nomedi ...
  • Python序列內置類型之元組類型詳解 1.元祖的概念 Python中的元組與列表類似,都是一個序列,不同的是元組的元素不能修改而已。 2.元組的創建 元組使用小括弧,列表使用方括弧。 註意:元組中只包含一個元素時,需要在元素後面添加逗號,否則括弧會被當作運算符使用。 3.Python元組操作 1. ...
  • 跳槽不算頻繁,但參加過不少面試(電話面試、face to face面試),面過大/小公司、互聯網/傳統軟體公司,麵糊過(眼高手低,缺乏實戰經驗,掛掉),也面過人,所幸未因失敗而氣餒,在此過程中不斷查缺補漏,養成了踏實、追本溯源、持續改進的習慣,特此將自己經歷過、構思過的一些面試題記錄下來,如果答案有 ...
  • 當一個Action完成它的任務後,通常需要返回一個實現IActionResult的對象,而最常見的就是View或者ViewResult,所謂的視圖對象。那麼視圖與最終所看到的頁面之間的聯繫又是怎樣形成的,這便是本文想要探討的問題。 在ResourceInvoker類之中,可以找到下列的代碼。這些代碼 ...
  • .net 信息採集ajax數據 關於.net信息採集的資料很多,但是如果採集的網站是ajax非同步載入數據的模式,又如何採集呢?今天就把自己做信息採集時,所遇到的一些問題和心得跟大家分享一下。 採集網站的幾種方式與利弊: 利用系統自帶HttpWebRequest對象,採集網站內容,優點是採集效率快,但 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...