zoukankan      html  css  js  c++  java
  • 匿名委托变量捕获的陷阱

    一个简单的例子:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             for (var i = 0; i < 3; i++)
     6             {
     7                 Task.Factory.StartNew(() => Func1(i));
     8             }
     9 
    10             Console.ReadKey();
    11         }
    12 
    13         static void Func1(int i)
    14         {
    15             Console.WriteLine(i);
    16         }
    17     }

    结果不是 0 1 2,而是3 3 3

    (补充:如果循环次数加大,会发现输出的并不都是相同的,但仍然不是预期)

    解释:

    匿名函数有捕获变量的特性

    闭包是可以包含自由(未绑定)变量的代码块;这些变量不是在这个代码块或者任何全局上下文中定义的,而是在定义代码块的环境中定义。

    参考资料:

    简单来讲,闭包允许你将一些行为封装,将它像一个对象一样传来递去,而且它依然能够访问到原来第一次声明时的上下文。这样可以使控制结构、逻辑操作等从调用细节中分离出来。访问原来上下文的能力是闭包区别一般对象的重要特征,尽管在实现上只是多了一些编译器技巧。

    我们知道,在匿名方法或者lambda中,可以访问或者修改该匿的定义范围内的变量。例如:

    1. int num = 1;   
    2. Func<int> incNum = () => ++num; 

    其中lambda表达式使用了在其外部定义的变量num。我们可以认为该段lambda语句块构成了一个闭包,而这个闭包捕获了外部变量num。

    好了,不说那么多让人看着难受的定义套话了。我们进入正题,看看在C#中变量是如何被捕获的。来看一个例子:

    1. public Func<String> CreateFunction()   
    2. {   
    3. String str = "我的幸运数字是";   
    4. int num = 17;   
    5. Func<String> func = () => str + num;   
    6. return func;   

    在这个例子中,定义了一个返回一个函数的方法CreateFunction。返回的函数构成了一个闭包,该闭包捕获了两个变量:String类型的str和int类型的num。

    好了,我们现在可以这样使用这个函数了:

    1. Func<String>   
    2. myFunc = CreateFunction();   
    3. String result = myFunc();  

    我们来分析一下这两行代码实际都干了什么。第一行很容易理解,我们把方法CreateFunction生成的匿名函数赋值给了委托myFunc。

    第二行更好理解,我们执行了myFunc,并将返回结果赋值给了变量result。我们再深入思考一下:在执行myFunc的时候,会访问到在CreateFunction中定义两个变量str与num。

    虽然这时CreateFunction的栈帧早就被销毁了,其内部定义的变量至今也“生死不明”了,但是因为我们知道这两个变量已经被闭包所捕获了,所以我们坚信这两个变量截至目前为止还是可以访问的!

    对于str对象,鉴于它是一个引用类型,所以只要有存在某个“东西”一直保存着对它的引用,它就不会被销毁。这样我们完全不用担心在我们需要它时,编译器或运行时会告诉我们它被弄丢了。

    然而对于num,情况就有些不同了。num是一个值类型。我们知道值类型是存活在栈上的,我们也知道它所存在的那个栈帧(也就是CreateFunction的帧)在CreateFunction执行完毕后就会被销毁,然后其上存在的任何值类型也会被一并的销毁,这其中当然包括我们所关注的变量num了。

    那么,我们为什么还能安全的访问num呢?C#中的变量捕获机制究竟有什么神奇之处,可以让值类型拥有违反常规的生存周期呢?装箱!你可能会立刻想到,把每个值类型都装到一个对象里,我们就可以让这个值类型拥有和那个包裹它的对象相同的寿命了。

    不过,这并不是C#实现者所选择的方式!C#并不会对每个需要捕获的值类型变量进行装箱操作,而是把所有捕获的变量统统放到同一个大“箱子”里——当编译器遇到需要变量捕获的情况时,它会默默地在后台构造一个类型,这个类型包含了每一个闭包所捕获的变量(包括值类型变量和引用类型变量)作为它的一个公有字段。这样,编译器就可以

    维护那些在匿名函数或lambda表达式中出现的外部变量了。

    更进一步,如果我们使用ILDASM工具查看CreateFunction方法的IL代码,我们会发现编译器压根就没有声明num和str变量。取而代之的是声明了一个类型名和实例名都及其难看的包装对象。这个玩意儿就是我们上面所说的那个被编译器默默生成,保存了所有捕获变量的引用的对象。

    我们还可以看到,在CreateFunction方法,C#源代码内所有对str和num的操作,在IL中都被转换成了对包装对象的同名公有成员的操作。顺便说一句,就连我们构造的那个lambda表达式“() => str + num”现在都被编译器转换成了这个包装对象的一个方法!

  • 相关阅读:
    telnet模拟http訪问
    network: Android 网络推断(wifi、3G与其它)
    Cocos2d-x学习笔记(19)(TestCpp源代码分析-3)
    Thinkphp编辑器扩展类kindeditor用法
    逛自己的微博,回想以前的那个“我”
    微信生成二维码
    [C++]四种方式求解最大子序列求和问题
    Android 颜色渲染(二) 颜色区域划分原理与实现思路
    Android 颜色渲染(一) 颜色选择器 ColorPickerDialog剖析
    Android 图标上面添加提醒(二)使用开源UI类库 Viewbadger
  • 原文地址:https://www.cnblogs.com/sohobloo/p/3517604.html
Copyright © 2011-2022 走看看