C#精粹--闭包陷阱

闭包定义

闭包(closure)在很多语言中都存在,在C#中,闭包是由匿名函数来表示的。C#中的闭包也叫做捕获的变量。当一个匿名函数引用了他所在作用域(一般情况下是一个方法)的局部变量时,为了能够顺利的执行匿名函数而不至于包含它的函数执行完之后线程栈弹出导致局部变量消失,会将这个变量的生命周期延长。这时就形成了闭包。闭包利用了匿名函数的一个特性:因为编译器会为匿名函数生成一个类(或结构),所以,提升匿名函数捕获的这个变量的生命周期的方法就是在把这个变量放到这个类中。此外,这个类中定义的方法既是这个匿名函数。

示例

for循环中的闭包陷阱

我们在使用lambda的时候会遇到闭包,在闭包中有一个陷阱是在for循环中产生的,先上代码:

class Program
    {
        static void Main(string[] args)
        {
            Action[] actions=new Action[5];
            for (int i = 0; i < actions.Length; i++)
            {
                actions[i] = () => Console.WriteLine(i);
            }
            foreach (var item in actions)
            {
                item();
            }          
            Console.ReadKey();
        }
    }

此时会看到Console输出的是一连串的5,这是因为C#中在for块中定义的int i会被当作外部变量来处理,我们在循环内部使用lambda的时候编译器会给我们生成一个类,比如这个代码如果是在Program中的main方法执行的时候这个类会在Program中生成,成为Program的一个内部类。这个类的主要作用是承载lambda表达式所代表的方法。当lambda表达式引用了一个局部变量时,为了保证这个变量的生命周期,这个局部变量会被编译器生成的这个类所捕捉,也就是说,这个局部变量的生命周期得到了提升,成为了一个类级别的字段了。

模拟闭包

我想说的是如何避免这个for循环中闭包的陷阱呢?先来模拟一下编译器在lambda背后的行为:

class Program
    {
        static void Main(string[] args)
        {
            Action[] actions = new Action[5];
            var innerClass = new InnerClass();//关键在这里
int i;//for循环中定义的局部变量是被当作外部变量来使用的,这是在C#中的实现。 for (i = 0; i < actions.Length; i++) { innerClass.i = i; actions[i] = innerClass.DoIt; } foreach (var item in actions) { item(); } Console.ReadKey(); } public class InnerClass//这里是模拟编译器为lambda表达式生成的类,我暂时命名为InnerClass,实际上编译器生成的这个内部类有自己的命名规则。 { public int i;//这个是捕获的for循环中的那个变量。 public void DoIt() { Console.WriteLine(i); } } }

闭包产生的这个陷阱关键就在于:

 var innerClass = new InnerClass();//关键在这里

避免闭包陷阱

上面这句代码的位置,之所以会产生陷阱,就是因为innerClass捕获到的是最后的那个变量i的值,说到这里就不难想象如何去避免这个陷阱了,我们可以在for循环内部定义一个变量来保存每次的循环变量i的值:

 

 class Program
    {
        static void Main(string[] args)
        {
            Action[] actions = new Action[5];
            
            for (int i = 0; i < actions.Length; i++)
            {
                int j = i;//关键这里
                actions[i] = Console.WriteLine(j);
            }
            foreach (var item in actions)
            {

                item();
            }
            Console.ReadKey();
        }   
    }

 

我们在for循环的内部使用了一个变量先来捕获一遍i,然后编译器会将这个生成的类放在循环内部(而不是在for循环外部生成),每循环一次就生成一个新的来捕获。牛逼吧编译器?

posted @ 2018-02-01 17:28  wall-ee  阅读(2285)  评论(2编辑  收藏  举报