C#中的閉包和意想不到的坑

雖然閉包主要是函數式編程的玩意兒,而C#的最主要特徵是面向對象,可是利用委託或lambda表達式,C#也能夠寫出具備函數式編程風味的代碼。一樣的,使用委託或者lambda表達式,也能夠在C#中使用閉包。編程

根據WIKI的定義,閉包又稱語法閉包或函數閉包,是在函數式編程語言中實現語法綁定的一種技術。閉包在實現上是一個結構體,它存儲了一個函數(一般是其入口地址)和一個關聯的環境(至關於一個符號查找表)。閉包也能夠延遲變量的生存週期。閉包

嗯。。看定義好像有點迷糊,讓咱們看看下面的例子吧編程語言

class Program
    {
        static Action CreateGreeting(string message)
        {
            return () => { Console.WriteLine("Hello " + message); };
        }

        static void Main()
        {
            Action action = CreateGreeting("DeathArthas");
            action();
        }
    }

這個例子很是簡單,用lambda表達式建立一個Action對象,以後再調用這個Action對象。
可是仔細觀察會發現,當Action對象被調用的時候,CreateGreeting方法已經返回了,做爲它的實參的message應該已經被銷燬了,那麼爲何咱們在調用Action對象的時候,仍是可以獲得正確的結果呢?
 
原來奧祕就在於,這裏造成了閉包。雖然CreateGreeting已經返回了,可是它的局部變量被返回的lambda表達式所捕獲,延遲了其生命週期。怎麼樣,這樣再回頭看閉包定義,是否是更清楚了一些?
 
閉包就是這麼簡單,其實咱們常常都在使用,只是有時候咱們都不自知而已。好比你們確定都寫過相似下面的代碼。函數式編程

void AddControlClickLogger(Control control, string message)
{
	control.Click += delegate
	{
		Console.WriteLine("Control clicked: {0}", message);
	}
}

這裏的代碼其實就用了閉包,由於咱們能夠確定,在control被點擊的時候,這個message早就超過了它的聲明週期。合理使用閉包,能夠確保咱們寫出在空間和時間上面解耦的委託。
 
不過在使用閉包的時候,要注意一個陷阱。由於閉包會延遲局部變量的生命週期,在某些狀況下程序產生的結果會和預想的不同。讓咱們看看下面的例子。函數

class Program
    {
	static List<Action> CreateActions()
        {
            var result = new List<Action>();
            for(int i = 0; i < 5; i++)
            {
                result.Add(() => Console.WriteLine(i));
            }
            return result;
        }

        static void Main()
        {
            var actions = CreateActions();
            for(int i = 0;i<actions.Count;i++)
            {
                actions[i]();
            }
        }
    }

這個例子也很是簡單,建立一個Action鏈表並依次執行它們。看看結果
code

相信不少人看到這個結果的表情是這樣的!!難道不該該是0,1,2,3,4嗎?出了什麼問題?對象

刨根問底,這兒的問題仍是出如今閉包的本質上面,做爲「閉包延遲了變量的生命週期」這個硬幣的另一面,是一個變量可能在不經意間被多個閉包所引用。blog

在這個例子裏面,局部變量i同時被5個閉包引用,這5個閉包共享i,因此最後他們打印出來的值是同樣的,都是i最後退出循環時候的值5。生命週期

要想解決這個問題也很簡單,多聲明一個局部變量,讓各個閉包引用本身的局部變量就能夠了。開發

//其餘都保持與以前一致
        static List<Action> CreateActions()
        {
            var result = new List<Action>();
            for (int i = 0; i < 5; i++)
            {
                int temp = i; //添加局部變量
                result.Add(() => Console.WriteLine(temp));
            }
            return result;
        }

這樣各個閉包引用不一樣的局部變量,剛剛的問題就解決了。

除此以外,還有一個修復的方法,在建立閉包的時候,使用foreach而不是for。至少在C# 7.0 的版本上面,這個問題已經被注意到了,使用foreach的時候編譯器會自動生成代碼繞過這個閉包陷阱。

//這樣fix也是能夠的
        static List<Action> CreateActions()
        {
            var result = new List<Action>();
            foreach (var i in Enumerable.Range(0,5))
            {
                result.Add(() => Console.WriteLine(i));
            }
            return result;
        }

這就是在閉包在C#中的使用和其使用中的一個小陷阱,但願你們能經過老胡的文章瞭解到這個知識點而且在開發中少走彎路!

相關文章
相關標籤/搜索