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

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

使用委託或者lambda表達式,也可以在c#中使用閉包。

最后更新 2022/6/15 下午10:49
老胡写代码
预计阅读 4 分钟
分类
.NET
标签
.NET 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 鍊表並依次執行它們。看看結果

5
5
5
5
5

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

刨根問底,這兒的問題還是出現在閉包的本質上面,作為“閉包延遲了變量的生命周期”這個硬幣的另外一面,是一個變量可能在不經意間被多個閉包所引用。

在這個例子裡面,局部變量 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;
}
0
1
2
3
4

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

除此之外,还有一个修复的方法,在创建闭包的时候,使用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#中的使用和其使用中的一個小陷阱,希望大家能通過老胡的文章了解到這個知識點並且在開發中少走彎路!

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/2/7

aot使用經驗總結

從項目創建伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 aot 發布測試。

继续阅读