详细介绍C#编译器

本文讲述C#编译器的一些问题,目的是防止错误使用本地变量。但是据我研究,这里面有“Bug”(注意双引号),那么会有什么有趣的“Bug”呢?首先大家看下一个简单的例子:

创新互联坚持“要么做到,要么别承诺”的工作理念,服务领域包括:成都网站设计、成都网站制作、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的蒙城网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!

 
 
  1. publicvoidTest()
  2. {
  3. {
  4. inta;
  5. }
  6. {
  7. inta;
  8. }
  9. }

在这个Test函数里面有两对打括号,标明两个互不相属的子范围。这里大家也许看的非常不习惯,因为没有人光秃秃的写这么两对大括号的。我跟大家说:没关系,编译器承认光秃秃的大括号的,这个也是标准C里面的规范之一,作用就是把大括号里面的所有东西认为是“一句话”,准确点讲是逻辑语句,同时内部是一个范围,约束范围内的本地变量不会往外传播。如果大家实在看不习惯了,可以自行加上诸如while(true)之类的前缀,就习惯了。

那么这段代码有什么Bug呢?没有,确实没有Bug,编译顺利通过。当然,显示了两个Warning,说a没有被用到,无伤大雅。我们首先来分析一下,编译器怎么给把这个给弄通过的呢?我们用Reflector来看一下(当然,因为没有切实的代码,所以只能够看IL,而不能够看C#):

 
 
  1. publichidebysiginstancevoidTest()cilmanaged
  2. {
  3. //CodeSize:2byte(s)
  4. .maxstack0
  5. .locals(
  6. int32num1,
  7. int32num2)
  8. L_0000:nop
  9. L_0001:ret
  10. }

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

 
 
  1. publicintTest()
  2. {
  3. intb;
  4. b=newRandom().Next(5);
  5. if(b<5)
  6. {
  7. inta=newRandom().Next(5);
  8. Console.WriteLine(a);
  9. b=a;
  10. }
  11. else
  12. {
  13. inta=newRandom().Next(10);
  14. Console.WriteLine(a);
  15. b=a;
  16. }
  17. returnb;
  18. }

Reflector 反编译结果:

 
 
  1. publicintTest()
  2. {
  3. intnum1=newRandom().Next(5);
  4. if(num1<5)
  5. {
  6. intnum2=newRandom().Next(5);
  7. Console.WriteLine(num2);
  8. returnnum2;
  9. }
  10. intnum3=newRandom().Next(10);
  11. Console.WriteLine(num3);
  12. returnnum3;
  13. }

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

分析完上面这些基本知识,我就来劲了:

 
 
  1. publicvoidTest()
  2. {
  3. {
  4. inta;
  5. }
  6. {
  7. inta;
  8. }
  9. inta;
  10. }

看,编译出来之后却出现了错误:
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当作另外一个变量。这到底是怎么回事呢?我们需要作本次探索的最后一个实验:

 
 
  1. publicvoidTest()
  2. {
  3. a=2;
  4. {
  5. inta;
  6. }
  7. {
  8. inta;
  9. }
  10. inta;
  11. }

这下可好,除了刚才那个错误之外,还多出来另外一个:
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#编译器在这些问题方面确实有相当严谨的考虑,不过我还是觉得有一些“悖论”存在,如果能够更加严谨,我认为只会更好。

当前题目:详细介绍C#编译器
文章位置:http://www.shufengxianlan.com/qtweb/news18/269068.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联