理解C#中的閉包

一、 閉包的含義

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

在看過的對於閉包的定義中,我的以爲比較清晰的是在《JavaScript高級程序設計》這本書中看到的。具體定義以下:編程

閉包是指有權訪問另外一個函數做用域中的變量的函數c#

注意,閉包這個詞自己指的是一種函數。而建立這種特殊函數的一種常見方式是在一個函數中建立另外一個函數。緩存

二、 在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有沒有被當作返回值和閉包的定義無關。就算它沒有被返回到外部,它依舊是個閉包。優化

三、 理解閉包的實現原理

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

第一次執行委託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中建立了它實例。局部變量其實是做爲匿名類中的字段存在的。

四、 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));
}

上述這兩點變化在必定程度上可以帶來性能的提高,目前的理解是,用結構體代替類,結構體實例可以在方法跑完後就當即釋放,不須要等待垃圾回收,因此在官方的推薦中,若是委託的使用不是必要的,更推薦使用本地函數而非匿名函數。

若是本博客描述的內容存在問題,但願你們可以提出寶貴的意見。堅持寫博客,從這一篇開始。

相關文章
相關標籤/搜索