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...

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  207 随笔 :: 19 文章 :: 1637 评论 :: 14 引用
我们先来看看展波举的例子:
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 阅读(1913) 评论(5)  编辑 收藏 所属分类: .NET 技术内幕

评论

#1楼  2004-10-26 13:53 dudu      
“可以自行加上诸如while(true)之类的前缀”, 应该是加上If(true)吧。
  回复  引用  查看    

#2楼  2004-10-26 14:51 msolap      
第3个a定义的出现,应该给出Error,这是由C# Spec. 规定的。

第1段代码的错误,和declaration space相关
可以参见C# Spec. 3.3

•... The local variable declaration space of a block includes any nested blocks. Thus, within a nested block it is not possible to declare a local variable with the same name as a local variable in an enclosing block.

至于第2段代码的错误信息,是和Scope相关
参见C# spec. 3.7
• The scope of a local variable declared in a local-variable-declaration is the block in which the declaration occurs. It is an error to refer to a local variable in a textual position that precedes the variable-declarator of the local variable.

另外,3.7特地提到
Scopes can be nested, and an inner scope may redeclare the meaning of a name from an outer scope. (This does not, however, remove the restriction imposed by §3.3 that within a nested block it is not possible to declare a local variable with the same name as a local variable in an enclosing block.)
  回复  引用  查看    

#3楼  2004-11-04 08:50 吕震宇      
我在给学生上课时,是这样讲的:一个变量的作用范围就是包含该变量的最内层大括号的范围。这样解释虽然有些愚蠢,但对付一般问题就够了。没想到Sumtec把这个问题分析的这么透彻。
  回复  引用  查看    

#4楼  2006-12-15 20:11 小鱼儿      
恩不错看看
  回复  引用  查看    

#5楼  2008-07-13 10:45 阿狗蛋 [未注册用户]
一点趣味也没有,一个人在演戏一样,人格分裂似的,没看出你的观点和想表达什么。
  回复  引用    


标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2004-10-26 13:30 编辑过
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: