“简单的.NET面试题”?以及IL代码的用途

开发 后端
一段看似简单的VB.NET代码,却输出了令人意想不到的结果。本文通过对这道“简单的.NET面试题”进行分析和IL代码跟踪,介绍了C#和VB.NET编译器的一些不同,以及跟踪调试技能的重要性。

.NET面试题?

我的一名很好学的学生给我发来了一封邮件,其内容如下:

==========================================================

你好!

感谢你给我的帮助!

有一个问题向你请教:

  1. for i as integer =1 to 10  
  2.   dim a as integer  
  3.   a=a+1  
  4. next  

在第二次循环结束时,a的值为多少?你是如何理解的?

非常感谢!

                              张XX

                             2009-8-12

============================================================

这是一段VB.NET代码,虽然我在开发中不太有可能写过这样子的代码——将一个变量的定义语句放到循环语句的内部,但作为一名老的VB程序员,这道题看上去太简单了,答案似乎一目了然。然而,如果真是这么简单,这名学生会费这么大功夫给我发这样一个邮件?

先不看答案,大家猜猜,结果是什么?

(空掉数行,别偷看答案!)

……

……

……

……

……

……

……

……

……

……

真实结果是:2!相信这是一个会让C#程序员大感意外的结果!难道不是每次循环开始时都新定义一个变量吗?新定义的变量应该取默认值0啊,为何会得到2?

有关“.NET面试题”的分析

为了便于分析,我将代码修改了一下,同时写了一段C#和VB.NET代码作为对比:

VB.NET代码:

  1. Module Module1  
  2.    
  3.     Sub Main()  
  4.         For i As Integer = 1 To 10  
  5.             Dim a As Integer 
  6.             a = a + 1  
  7.             Console.WriteLine(a)  
  8.         Next 
  9.         Console.ReadKey()  
  10.     End Sub 
  11.    
  12. End Module 
  13.   

C#代码:

  1. class Program  
  2.     {  
  3.         static void Main(string[] args)  
  4.         {  
  5.             for (int i = 1; i <= 10; i++)  
  6.             {  
  7.                 int a = 0;  //必须初始化,否则C#编译器报错!  
  8.                 a=a+1;  
  9.                 Console.WriteLine(a);  
  10.             }  
  11.             Console.ReadKey();  
  12.         }  
  13.     } 

运行结果是:VB.NET程序输出1到10,而C#程序输出10个“1”。

原因何在?

有的程序员可能会想到可以使用Reflector工具反汇编上述两段代码生成的程序集,看看原因到底是什么。

然而你会很失望,对比结果看不出有什么大的差异,甚至Reflector根据IL指令为VB.NET程序生成的C#代码还是错的,无法通过C#编译器的编译。

“.NET面试题”全跟踪:IL代码解读

最后一招:祭出“终极武器”——ildasm,直接阅读生成的IL指令。

在Release模式下,VB.NET程序生成的IL代码如下(我加了详细的注释,注意红色的指令):

  1. .method public static void  Main() cil managed  
  2. {  
  3.   .entrypoint  
  4.   .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )  
  5.   // Code size       28 (0x1c)  
  6. .maxstack  2  
  7.    
  8. //分配两个Slot,用于保存两个局部变量,对于整型变量,初值为0  
  9.   .locals init ([0] int32 a,   
  10.            [1] int32 i)  
  11. //将“1”保存到变量”i”中  
  12.   IL_0000:  ldc.i4.1  
  13.   IL_0001:  stloc.1  
  14. //将变量a的当前值装入计算堆栈  
  15.   IL_0002:  ldloc.0  
  16.  //将“1” 装入计算堆栈  
  17.   IL_0003:  ldc.i4.1  
  18. //实现a=a+1,add.ovf指令从堆栈中弹出两个操作数相加,并进行溢出检查  
  19.   IL_0004:  add.ovf  
  20. //结果保存回变量a中  
  21.  IL_0005:  stloc.0  
  22. //将变量a的新值装入计算堆栈  
  23.   IL_0006:  ldloc.0  
  24. //将a的新值输出显示  
  25.   IL_0007:  call       void [mscorlib]System.Console::WriteLine(int32)  
  26.  //将变量i的新值装入计算堆栈  
  27.  IL_000c:  ldloc.1  
  28. //将”1”装入计算堆栈  
  29.   IL_000d:  ldc.i4.1  
  30. //实现i=i+1,循环变量自增  
  31.   IL_000e:  add.ovf  
  32.   //i的新值保存到变量i中  
  33.   IL_000f:  stloc.1  
  34. //将变量i的值装入计算堆栈  
  35.   IL_0010:  ldloc.1  
  36. //将循环终值10压入计算堆栈  
  37.   IL_0011:  ldc.i4.s   10  
  38. //如果i<=10,跳到指令IL_0002处重新执行。  
  39.   IL_0013:  ble.s      IL_0002  
  40. //暂停显示  
  41.   IL_0015:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()  
  42.   IL_001a:  pop  
  43. //退出  
  44.   IL_001b:  ret  
  45. // end of method Module1::Main 

而C#生成的如下,为简洁起见,我只在关键语句加了注释

  1. .method private hidebysig static void  Main(string[] args) cil managed  
  2. {  
  3.   .entrypoint  
  4.   // Code size       32 (0x20)  
  5.   .maxstack  2  
  6.   .locals init ([0] int32 i,  
  7.            [1] int32 a)  
  8. //i=1  
  9.   IL_0000:  ldc.i4.1  
  10.   IL_0001:  stloc.0  
  11. //无条件直接跳到IL_0014处!  
  12.   IL_0002:  br.s       IL_0014  
  13.    
  14. //a=0  
  15.   IL_0004:  ldc.i4.0  
  16.   IL_0005:  stloc.1  
  17. //a++  
  18.   IL_0006:  ldloc.1  
  19.   IL_0007:  ldc.i4.1  
  20.   IL_0008:  add  
  21.   IL_0009:  stloc.1  
  22. //输出a的值  
  23.   IL_000a:  ldloc.1  
  24.   IL_000b:  call       void [mscorlib]System.Console::WriteLine(int32)  
  25. //i++  
  26.   IL_0010:  ldloc.0  
  27.   IL_0011:  ldc.i4.1  
  28.   IL_0012:  add  
  29.   IL_0013:  stloc.0  
  30.   IL_0014:  ldloc.0  
  31. //如果i<=10,跳转到IL_0004处  
  32.   IL_0015:  ldc.i4.s   10  
  33.   IL_0017:  ble.s      IL_0004  
  34.   IL_0019:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()  
  35.   IL_001e:  pop  
  36. //结束返回  
  37.   IL_001f:  ret  
  38. // end of method Program::Main  
  39.   

情况很清楚了,VB.NET编译器充分利用了变量的默认值,没有生成直接的变量初始化语句,因此,它每次循环结束后跳到IL_0002处,其指令直接取出的就是变量a的当前值,因此,每次循环的结果都可以保留,程序输出结果“1”,“2”,……,“10”。

而C#则要求变量必须明确初始化,编译器为变量a生成了初始化语句(IL_0004到IL_0005),而这两个语句又在循环体内,每次循环开始a都回到初值0,因此,输出10个“1”。

在IL代码面前,编译器玩的把戏被揭穿!

事实上,C#从2.0开始,就出现了许多让不少初学者比较头痛的语法,比如匿名方法、Lambda表达等,其实,只要使用Reflector或者是ildasm工具,你会发现这些与传统语法相比“很奇怪”的新特性,在底层都会变成大家所熟悉的语法形式。

另外,从这个小实例中可以看到,掌握“比较底层”的IL编程,在了解.NET技术内幕方面还是有帮助的。同时提醒一下.NET学习者,在学习中要重视掌握跟踪调试的基本技能,我看到的几乎所有的软件高手,大都是分析问题的高手,他们高超技能之一往往表现为能熟练应用各种工具深入调试程序找到问题的关键,进而开发出优秀的程序。

本文来自bitfan(数字世界一凡人)的专栏:《一道可以成为.NET面试“必杀题”的“简单问题”》。

【编辑推荐】

  1. 大家一起探讨两道C#面试题
  2. 中高级ASP.NET程序员面试题目实例
  3. 史上最全ASP.NET面试题目集锦
  4. C#数据结构与算法之顺序表浅析
  5. C#算法一道面试题浅析
责任编辑:yangsai 来源: bitfan的博客
相关推荐

2014-09-19 11:17:48

面试题

2021-01-22 11:58:30

MySQL数据库开发

2017-12-22 13:38:55

2017-11-21 12:15:27

数据库面试题SQL

2009-08-28 09:29:02

2020-06-04 14:40:40

面试题Vue前端

2010-11-26 10:53:29

戴尔

2014-07-15 11:10:01

面试题面试

2020-09-21 11:10:06

Docker运维面试

2023-11-13 07:37:36

JS面试题线程

2011-03-24 13:27:37

SQL

2009-08-01 23:17:19

ASP.NET面试题目ASP.NET

2014-12-02 10:02:30

2009-06-02 15:30:35

Hibernate面试笔试题

2018-07-10 16:50:28

数据库MySQL面试题

2021-09-09 08:54:48

SpringAOP面试题AOP事务

2009-11-19 10:29:01

2023-09-21 14:55:24

Web 开发TypeScript

2015-09-02 09:32:56

java线程面试

2009-06-06 18:36:02

java面试题
点赞
收藏

51CTO技术栈公众号