理解C#中的閉包

黑洞視界發表於2018-08-25

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

上述這兩點變化在一定程度上能夠帶來效能的提升,目前的理解是,用結構體代替類,結構體例項能夠在方法跑完後就立即釋放,不需要等待垃圾回收,所以在官方的推薦中,如果委託的使用不是必要的,更推薦使用本地函式而非匿名函式。

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

相關文章