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#中的使用和其使用中的一个小陷阱,但愿你们能经过老胡的文章了解到这个知识点而且在开发中少走弯路!

相关文章
相关标签/搜索