c#闭包问题--踩过的一个大坑

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010639500/article/details/82385959

引言

     之前在做c#游戏开发的过程中,遇到了一个大坑。当时为了实现动态加载图鉴,用到了button的循环委托,谁知道这样就踩了c#闭包问题的大坑!现在对这个问题进行总结,避免下次再踩入这样的巨坑。
     先贴下结论:在C#中,原来闭包只是编译器玩的花招而已,它仍然没有脱离.NET对象生命周期的规则,它将需要修改作用域的变量直接封装到返回的类中变成类的一个属性,从而保证了变量的生命周期不会随函数调用结束而结束,因为变量n在这里已经成了返回的类的一个属性。

闭包的概念

Q:先抛出第一个问题,什么是闭包?闭包会出现在怎样的场景中?
A:在c#中,闭包是这样定义的:内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。但该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。闭包主要用在访问外层函数定义的变量上。
     那么问题来了,笔者之前是学c和c++的,闭包的概念第一次接触,很懵逼。怎么办?继续查资料啊,笔者发现,闭包的概念首次提出是在Python语言中的,Python语言有一个很奇怪的地方,他可以嵌套定义函数,我们可以在定义函数a的时候,在a的内部定义函数b,且函数b只在函数a中有效。这就很神奇了,因为据笔者所知,c++和c是不支持函数嵌套定义的,只是支持函数的嵌套调用。嵌套定义和嵌套调用有个很大的区别,即嵌套定义中,内部函数可以访问其外部函数的作用域,但是外部函数不能访问内部函数的作用域。而c++中每个函数只可以使用自己内部栈中存储的局部变量、由入参传入的参数以及全局变量。Python中的函数嵌套定义的优势是能够对复杂代码进行划片,使得单个函数只实现单个功能,而且内部函数可以减少私有函数的定义;但是会使得函数间的耦合性增大。
     基于函数嵌套定义的需求,Python引入了闭包的概念。闭包(Closure)是词法闭包的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。这样的说法可能有些晦涩难懂,相信读者看了下面的例子应该就能对闭包的概念有个比较鲜明的认识了。

def funx(x):
	def funy(y):
		return x * y
    return funy

     上面的例子可以看出,函数funx里面又定义了一个新函数funy,这个新函数里面的一个变量正好是外部函数funx的参数。也就是说,外部传递过来的参数已经和funy函数绑定到一起了。我们可以把x看做函数funy的一个配置信息,配置信息不同,函数的功能就不一样了,也就是能得到定制之后的函数。
     相信经过上面的一番讲解,大家应该都对闭包的概念有所了解了吧。接下来,我们继续回到c#的闭包问题中。由于c#中有委托和lambda函数,所以c#其实也可以在函数的定义中利用lambda或者委托实现函数的嵌套定义, 此时闭包可以帮助我们轻松地访问外层函数定义的变量
Q:好,既然闭包这么有用,我们直接使用这个概念就好了嘛,还有什么需要注意的呢?
A:就像我们在回答闭包的概念时就提出的问题一样,内层函数引用外层函数的变量不是会随外层函数变量的变化而变化的,只会引用外层函数的变量的最终值。
Q:那我们怎么样才能做到让内层函数的引用值随着外层函数变量的变化而变化呢?从而避免闭包陷阱呢?
A:C#中普遍的做法是,将匿名函数引用的变量用一个临时变量保存下来,然后在匿名函数中使用临时变量。

错误例子,产生了闭包陷阱:

List<UserModel> userList = new List<UserModel>
            {
                new UserModel{ UserName="jiejiep", UserAge = 26},
                new UserModel{ UserName="xiaoyi", UserAge = 25},
                new UserModel{ UserName="zhangzetian", UserAge=24}
            };

            for(int i = 0 ; i < 3; i++)
            {
                ThreadPool.QueueUserWorkItem((obj) =>
                {
                    Thread.Sleep(1000);
                    UserModel u = userList[i];//i永远都是userList.Count
                    Console.WriteLine(u.UserName);
                });
            }

正确例子,解决了闭包陷阱:

List<UserModel> userList = new List<UserModel>
            {
                new UserModel{ UserName="jiejiep", UserAge = 26},
                new UserModel{ UserName="xiaoyi", UserAge = 25},
                new UserModel{ UserName="zhangzetian", UserAge=24}
            };

            for(int i = 0 ; i < 3; i++)
            {
                UserModel u = userList[i];
                ThreadPool.QueueUserWorkItem((obj) =>
                {
                    Thread.Sleep(1000);
                    Console.WriteLine(u.UserName);
                });
            }

A:但是为什么用临时变量就能够避免闭包陷阱呢?临时变量不也是外层函数的变量么?他也会变化啊,不应该也是只会引用他的最终值么?
Q:这是因为所谓的闭包,就如之前的定义所说,是引用了自由变量的函数,我们闭包的是“变量”,而不是“值”;而()=>v则是返回v的“当前值”,而不是创建该委托时v的“返回值”。所以在“for”循环中的添加的匿名函数,只是返回了变量i 而不是i的值。所以在第一个错误的例子中,当Lambda表达式被真正执行时,i已经是values.Count 值啦,所以会抛出“超出索引范围”。而在第二个正确的例子里,在每一次循环中,会创建一个新的临时变量u来保存当前循环的i,当委托或Lambda创建时,闭包这个新的临时变量u,且每个不同委托引用的u是互相独立的,其到委托执行时都是不会变化的,所以使用临时变量来保存匿名函数想要引用的外部函数变量可以解决闭包陷阱。

     为了帮助读者对闭包有更进一步的理解,这里我引用下jujusharp大大的原文,他在c#与闭包这篇文章中对c#的闭包做了深刻的解释,原文如下
     闭包其实就是使用的变量已经脱离其作用域,却由于和作用域存在上下文关系,从而可以在当前环境中继续使用其上文环境中所定义的一种函数对象。
     你可能会好奇.net本身并不支持函数对象,那么这样的特性又是从何而来呢?答案是编译器,我们一看IL代码便会明白了。
     首先我给出c#代码:

public class TCloser {
        public Func<int> T1(){
            var n = 10;
            return () =>
            {
                return n;
            };
        }
 
        public Func<int> T4(){
            return () =>
            {
                var n = 10;
                return n;
            };
        }
    }

     这两个返回的匿名函数的唯一区别就是返回的委托中变量n的作用域不一样而已,T1中变量n是属于T1的,而在T4中,n则是属于匿名函数本身的。但我们看看IL代码就会发现这里面的大不同了:

.method public hidebysig instance class [mscorlib]System.Func`1<int32> T1() cil managed{
    .maxstack 3
    .locals init (
        [0] class ConsoleApplication1.TCloser/<>c__DisplayClass1 CS$<>8__locals2,
        [1] class [mscorlib]System.Func`1<int32> CS$1$0000)
    L_0000: newobj instance void ConsoleApplication1.TCloser/<>c__DisplayClass1::.ctor()
    L_0005: stloc.0
    L_0006: nop
    L_0007: ldloc.0
    L_0008: ldc.i4.s 10
    L_000a: stfld int32 ConsoleApplication1.TCloser/<>c__DisplayClass1::n
    L_000f: ldloc.0
    L_0010: ldftn instance int32 ConsoleApplication1.TCloser/<>c__DisplayClass1::<T1>b__0()
    L_0016: newobj instance void [mscorlib]System.Func`1<int32>::.ctor(object, native int)
    L_001b: stloc.1
    L_001c: br.s L_001e
    L_001e: ldloc.1
    L_001f: ret
}
 
.method public hidebysig instance class [mscorlib]System.Func`1<int32> T4() cil managed
{
    .maxstack 3
    .locals init (
        [0] class [mscorlib]System.Func`1<int32> CS$1$0000)
    L_0000: nop
    L_0001: ldsfld class [mscorlib]System.Func`1<int32> ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
    L_0006: brtrue.s L_001b
    L_0008: ldnull
    L_0009: ldftn int32 ConsoleApplication1.TCloser::<T4>b__3()
    L_000f: newobj instance void [mscorlib]System.Func`1<int32>::.ctor(object, native int)
    L_0014: stsfld class [mscorlib]System.Func`1<int32> ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
    L_0019: br.s L_001b
    L_001b: ldsfld class [mscorlib]System.Func`1<int32> ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
    L_0020: stloc.0
    L_0021: br.s L_0023
    L_0023: ldloc.0
    L_0024: ret
}

     看IL代码你就会很容易发现其中究竟了,在T1中,函数对返回的匿名委托构造的是一个类,名称为newobj instance void ConsoleApplication1.TCloser/<>c__DisplayClass1::.ctor(),而在T4中,则是仍然是一个普通的Func委托,只不过级别变为类级别了而已。
     那我们接着看看T1中声明的类c__DisplayClass1是何方神圣:

.class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
    extends [mscorlib]System.Object{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed{}
    .method public hidebysig instance int32 <T1>b__0() cil managed{}
    .field public int32 n
}

     看到这里想必你已经明白了,在C#中,原来闭包只是编译器玩的花招而已,它仍然没有脱离.NET对象生命周期的规则,它将需要修改作用域的变量直接封装到返回的类中变成类的一个属性n,从而保证了变量的生命周期不会随函数T1调用结束而结束,因为变量n在这里已经成了返回的类的一个属性了。
     C#中,闭包其实和类中其他属性、方法是一样的,它们的原则都是下一层可以畅快的调用上一层定义的各种设定,但上一层则不具备访问下一层设定的能力。即类中方法里的变量可以自由访问类中的所有属性和方法,而闭包又可以访问它的上一层即方法中的各种设定。但类不可以访问方法的局部变量,同理,方法也不可以访问其内部定义的匿名函数所定义的局部变量。
     这正是C#中的闭包,它通过超越java语言的委托打下了闭包的第一步基础,随后又通过各种语法糖和编译器来实现如今在.NET世界全面开花的Lamda和LINQ。也使得我们能够编写出更加简洁优雅的代码。

注意点

     在c#5.0后,for和foreach在处理闭包问题上有了一些新的改变。为了适应不同的需求,微软对佛 foreach做了调整,“foreach”的遍历中定义的临时循环变量会被逻辑上限制在循环内,“foreach”的每次循环都会是循环变量的一个拷贝,这样闭包就看起来关闭了(没有了)。但“for”循环没有做修改。代码示例如下:

namespace Test1
{
    delegate void Func();
    public class TestFor
    {
        public void test()
        {
            List<Func> l = new List<Func>();
            for(int i = 0;i<5;i++)
            {
                l.Add(() =>
                {
                    Console.WriteLine(i);
                });
            }

            for(int i=0;i<5;i++)
            {
                l[i]();
            }
        }
    }

    public class TestForeach
    {
        public void test()
        {
            List<Func> l = new List<Func>();
            int[] a={0,1,2,3,4};
            foreach(int i in a)
            {
                l.Add(() =>
                {
                    Console.WriteLine(i);
                });
            }

            for (int i = 0; i < 5; i++)
            {
                l[i]();
            }
        }
    }

    public class main
    {
        public static void Main()
        {
            TestFor t1 = new TestFor();
            TestForeach t2 = new TestForeach();
            Console.WriteLine("TestFor");
            t1.test();
            Console.WriteLine("TestForeach");
            t2.test();
        }
    }
}

     代码结果如下:

TestFor
5
5
5
5
5
TestForeach
0
1
2
3
4

扩展认识

     笔者后来又想了下,既然闭包是由于函数嵌套定义引起的,c#中闭包存在于lambda和委托的情况下,那么现在引入了lambda的c++中也应该存在闭包!
     经过笔者的一番搜索,发现c++中的确也存在闭包的概念。c++ 里使用闭包有3个办法:(1)operator();(2)lambda表达式;(3)boost::bind/std::bind。c++中的闭包和c#中的闭包大致相同,这里就不做过多介绍了,感兴趣的读者可以访问https://www.cnblogs.com/Aion/p/3449756.html来进行阅读。

引用

博客园:http://www.cnblogs.com/jujusharp/archive/2011/08/04/2127999.html
博客园:https://www.cnblogs.com/HQFZ/p/4903400.html
博客园:https://www.cnblogs.com/jiejie_peng/p/3701070.html
博客园:https://www.cnblogs.com/Aion/p/3449756.html

猜你喜欢

转载自blog.csdn.net/u010639500/article/details/82385959
今日推荐