SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
我们先来看看展波举的例子:
http://blog.joycode.com/zhanbos/archive/2004/10/26/36605.aspx
在这个例子里面我们看到,编译器会检查scope问题,目的是防止错误使用本地变量。但是据我研究,这里面有“Bug”(注意双引号),那么会有什么有趣的“Bug”呢?我来给大家一个简单的例子:

        public void Test()
        
{
            
{
                
int a;
            }

            
{
                
int a;
            }

        }


在这个Test函数里面有两对打括号,标明两个互不相属的子范围。这里大家也许看的非常不习惯,因为没有人光秃秃的写这么两对大括号的。我跟大家说:没关系,编译器承认光秃秃的大括号的,这个也是标准C里面的规范之一,作用就是把大括号里面的所有东西认为是“一句话”,准确点讲是逻辑语句,同时内部是一个范围,约束范围内的本地变量不会往外传播。如果大家实在看不习惯了,可以自行加上诸如while(true)之类的前缀,就习惯了。
那么这段代码有什么Bug呢?没有,确实没有Bug,编译顺利通过。当然,显示了两个Warning,说a没有被用到,无伤大雅。我们首先来分析一下,编译器怎么给把这个给弄通过的呢?我们用Reflector来看一下(当然,因为没有切实的代码,所以只能够看IL,而不能够看C#):
.method public hidebysig instance void Test() cil managed
{
      
// Code Size: 2 byte(s)
      .maxstack 0
      .locals (
            int32 num1,
            int32 num2)
      L_0000: nop 
      L_0001: ret 
}


 

哦!原来编译器把内部的变量改名字了!或者说编译器把他们当作完全不同的两个变量来对待。同时我们在这里也可以看出来,实际上在IL里面时不区分范围的,只有本地变量着一个简单的概念。无论你在哪个范围,在什么时候开始声明,实际上都是在函数的一开始用一个.locals这样的伪语句来声明的。这么做是简单省事的办法,因为如果在用户源代码实际声明的地方才在栈上面开辟空间,那么最后函数退出的时候就不知道该释放多少栈空间了。当然这不是不可以解决的,但是那样的话增加了不必要的复杂度。如果我来设计.NET Framework,我也会通过高级语言的编译器来约束范围问题,而不是摆到IL里面去解决。(毕竟IL里面没有这样的功能不影响我们写程序)稍微引申一下,我们就知道,一个函数里面有多少个本地变量,取决于整个函数内部声明了多少本地变量,而与变量所在范围无关。在IL这一层里面暂时我们没有看到这样的优化工作,我们可以看看这样的代码最后被编译器编译成什么了(用Release模式编译):
        public int Test()
        
{
            
int b;
            b 
= new Random().Next(5);
            
if (b < 5)
            
{
                
int a = new Random().Next(5);
                Console.WriteLine(a);
                b 
= a;
            }

            
else
            
{
                
int a = new Random().Next(10);
                Console.WriteLine(a);
                b 
= a;
            }

            
return b;
        }


Reflector 反编译结果:
public int Test()
{
      
int num1 = new Random().Next(5);
      
if (num1 < 5)
      
{
            
int num2 = new Random().Next(5);
            Console.WriteLine(num2);
            
return num2;
      }

      
int num3 = new Random().Next(10);
      Console.WriteLine(num3);
      
return num3;
}

大家可以看到num1是b,num2和num3则是分别的两个a。事实上这两个a互相之间是没有任何冲突的,也就是说是完全可以重用的,编译原理里面也有一个变量重用的优化,但是这里看不到有这样的优化,我觉得比较吃惊。虽然说这也可以算是一种Bug(严格说来是也不是),但是我要说的“Bug”不是这个。

分析完上面这些基本知识,我就来劲了:
        public void Test()
        
{
            
{
                
int a;
            }

            
{
                
int a;
            }

            
int a;
        }

看,编译出来之后却出现了错误:
error CS0136: A local variable named 'a' cannot be declared in this scope because it would give a different meaning to 'a', which is already used in a 'child' scope to denote something else
哦,原来这个跟声明的顺序还没有关系,只要子范围里面有a了,那就不能够再定义这个变量了。这个难道跟IL里面所有变量都在函数开始部分声明有关系?看起来好像是这么一回事,但是实际上不是,因为C#的编译器完全可以像前面那样,把最后一个a当作另外一个变量。这到底是怎么回事呢?我们需要作本次探索的最后一个实验:
        public void Test()
        
{
            a 
= 2;
            
{
                
int a;
            }

            
{
                
int a;
            }

            
int a;
        }

这下可好,除了刚才那个错误之外,还多出来另外一个:
error CS0103: The name 'a' does not exist in the class or namespace 'ConsoleApplication1.Class2'
也就是说,编译器根本就没有把后面那个a当作从函数一开始的地方定义来看待。但是这两个错误合起来反而容易让我们产生这样的错觉和悖论:
因为前面两个a在范围外面就应该消失其影响力,那就不应该跟后面的a产生冲突。但现在既然你说了,第三个a的定义根前面那两个a的其中某一个定义相冲突了,那我就只能够认为后面这个a实际上在前两个a被定义出来之前就已经存在了,因为后面这个a处于外层范围,它不会在内层范围失去作用之前失效,这样还能够解释得通。可是这么解释我只能够认为外层的a应该在函数一开始的地方就生效了(老式的C编译器有一段时间确实是这样的),可是偏偏还来一个CS0103错误!解释不通,有“Bug”!

最后我来修正这个我一开始提出的说法,其实并没有Bug。得出有Bug的结论,那是从纯粹的语法角度看这个问题的,我也觉得应该容许在第三个a的定义出现,顶多只给出一个Warning。但是微软却给出了一个错误,我想这是从避免不必要的Bug的角度考虑,尽量保护开发人员避免不必要的烦恼。开发人员确实很有可能在定义了第三个a的时候忘记第一二个a已经失效了,同时也忘记了自己定义过第三个a,还以为自己用的是第一个或者第二个a里面的数据。不过对于这种解释,我还是有意见的:既然约束已经缩窄到这个地步了,那为什么要允许第二个a的定义呢?如果开发人员会忘记自己定义过第三个a,有什么理由认为不会把第二个a的定义给忘记了,以为自己在用第一个a呢?

本来上面所写的那些统统都是垃圾代码,我认为,在一个函数内部根本就不应该有相同的变量来迷惑自己。C#的编译器在这些问题方面确实有相当严谨的考虑,不过我还是觉得有一些“悖论”存在,如果能够更加严谨,我认为只会更好。
posted on 2004-10-26 13:05  Sumtec  阅读(4324)  评论(12编辑  收藏  举报