1. 程式人生 > >理解C#中的閉包

理解C#中的閉包

console 個人 並且 表達 另一個 程序 常見 實例 演示

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

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

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

理解C#中的閉包