zoukankan      html  css  js  c++  java
  • 利用Reflector把"闭包"看清楚

    今天老赵在园子里发了一篇文章"警惕匿名方法造成的变量共享",立即引起了大家的广泛关注(老赵就是园子的"人气天王",呵呵),而且这个问题园子里也有其它几篇文章做了研究
    比如"闭包""《你不常用的c#之三》:Action 之怪状 "

    如果只是停留在c#2.0/3.0的"简捷且优雅"的代码风格上,初学者确实难理解这个"怪"现象,前二天买了本anytao的“你必须知道的.net”,里面提供了一种研究这类表面"怪"现象的基本方法--IL分析,并推荐了大名鼎鼎的反编译工具"Reflector",下面利用这个工具对其分析一二(高手就不必看了,权当给初学者一些参考)

    原始代码一(摘自"《你不常用的c#之三》:Action 之怪状"一文):

    代码1
    using System;
    using System.Collections.Generic;

    namespace ConsoleTest
    {
        
    class Program
        {
            
    static void Main(string[] args)
            {
                List
    <Action> ls = new List<Action>();
                
    for (int i = 0; i < 10; i++)
                {                
                    ls.Add(() 
    => Console.WriteLine(i));                
                }

                
    foreach (Action action in ls)
                {
                    action();
                }
                System.Console.Read();
            }       
        }  

    }

    结果:一连输出了10行完全相同的"10"(可能并没有按代码编写者的"意图",输出0到9),why?

    打开Relector,先做一些设置:打开"View"菜单-->选择"Options",先去掉Show PDB symbols前的勾,然后把Optimization后的下拉框改为".Net 1.0"(众多的"语法糖",比如匿名方法,扩展方法等都是在1.0版本以后出现的,这样设置的目的是去掉这些华丽的外衣,直接反应出原始的c#代码),刚才的代码经过反编译后,大概如下:

    [CompilerGenerated]
    private sealed class <>c__DisplayClass2
    {
        
    // Fields
        public int i;

        
    // Methods
        public void <Main>b__0()
        {
            Console.WriteLine(
    this.i);
        }
    }

     
    private static void Main(string[] args)
    {
        List
    <Action> list = new List<Action>();
        Action item 
    = null;
        
    <>c__DisplayClass2 class2 = new <>c__DisplayClass2();
        class2.i = 0
    ;
        while (class2.i < 10
    )
        {
            if (item == null
    )
            {
                item = new Action(class2.<Main>
    b__0);
            }
            list.Add(item);
            class2.i++
    ;
        }
        
    foreach (Action action2 in list)
        {
            action2();
        }
        Console.Read();
    }

     可以看出,
    1.编译器自动生成了一个密封类:<>c__DisplayClass2,里面有一个公有字段i,以及一个公共方法<Main>b__0()--用来输出i
    2.再看Main方法中的高亮部分,自始至终,<>c__DisplayClass2就只生成了一个实例class2,至于下面的while里变来变去,也只不过在改变i这个变量(也就是实例class2的成员i),而我们知道“类(class)”是引用类型,实际上class2不过是个引用而已,所以每次用new Action(class2.<Main>b__0)生成item,再list.Add(item)进去后,每个item调用的都是同一个引用,因此最终一连输出10行相同的结果--即数字10,也就是理所当然了

    把代码1,稍作修改,如下: 

    using System;
    using System.Collections.Generic;

    namespace ConsoleTest
    {
        
    class Program
        {
            
    static void Main(string[] args)
            {
                List
    <Action> ls = new List<Action>();
                
    for (int i = 0; i < 10; i++)
                {
                    
    int lp = i;
                    ls.Add(() 
    => Console.WriteLine(lp));                
                }

                
    foreach (Action action in ls)
                {
                    action();
                }
                System.Console.Read();
            }       
        }  

    }
    即在循环内部用一个临时变量lp做了一个中转,这次运行的结果,屏幕上输出了0-9共10行不相同的结果
    why?
    还是用Reflector来看看到底最终的代码是啥?
    [CompilerGenerated]
    private sealed class <>c__DisplayClass1
    {
        
    // Fields
        public int tp;

        
    // Methods
        public void <Main>b__0()
        {
            Console.WriteLine(
    this.tp);
        }
    }


    private static void Main(string[] args)
    {
        List
    <Action> list = new List<Action>();
        
    for (int i = 0; i < 10; i++)
        {
            <>c__DisplayClass1 class2 = new <>
    c__DisplayClass1();
            class2.tp =
     i;
            list.Add(new Action(class2.<Main>
    b__0));
        }
        
    foreach (Action action in list)
        {
            action();
        }
        Console.Read();
    同样,编译器还是自动为我们生成了一个密封类,这一点跟代码1反编译后的一样,关注一下高亮部分,这回<>c__DisplayClass1 class2 = new <>c__DisplayClass1();是放在循环里写的,也就是说10次外循环走下来,一共创建了10个不同的c__DisplayClass1()实例,剩下的就不用多说了,看明白了吧
    关于对于这个现象,个人觉得老赵的建议很好:委托创建完后,即时使用--no problem!(其实代码1也可以改成这样)

    代码1修改后
    using System;
    using System.Collections.Generic;
    namespace ConsoleTest
    {
        
    class Program
        {
            
    static void Main(string[] args)
            {
                List
    <Action> ls = new List<Action>();
                
    for (int i = 0; i < 10; i++)
                {                
                    ls.Add(() 
    => Console.WriteLine(i));
                    ls[i]();
                }
               
                System.Console.Read();
            }       
        }  
    }
    结果正常,输出0到9,再一次验证了"立即使用"是没问题的,但如果不是立即使用,就得多想想了
    最后,其实本文所说的现象老赵在文中已经讲得很明白了,我在这里只不过向初学者推荐了一下反编译的基本分析方法(当然你如果懂IL的话,可以分析得更透),很多情况下,光看程序表面的现象,是很难想明白的,利用一些工具,找到表象下的本质相对更容易把握。
    作者:菩提树下的杨过
    出处:http://yjmyzz.cnblogs.com
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    动态规划解按摩师的最长预约时间
    C#中WinForm的Tab键顺序调整顺序
    内网穿透工具对比FRP+NPS+Zerotier与NAT服务器测试
    " " 和 ' ' 混用拼接html字符串,且含有事件,事件中有参数
    HAProxy在Windows下实现负载均衡与反向代理
    react 导入src外部的文件 Relative imports outside of src/ are not supported.
    11_实例
    C#删除指定目录下文件(保留指定几天前的日志文件)
    【转】系统创建定时执行任务bat批处理删除指定N天前文件夹的文件
    mariadb导如数据异常------Error Code: 1153
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/1410924.html
Copyright © 2011-2022 走看看