.NET面试题?
我的一名很好学的学生给我发来了一封邮件,其内容如下:
==========================================================
你好!
感谢你给我的帮助!
有一个问题向你请教:
- for i as integer =1 to 10
- dim a as integer
- a=a+1
- next
在第二次循环结束时,a的值为多少?你是如何理解的?
非常感谢!
张XX
2009-8-12
============================================================
这是一段VB.NET代码,虽然我在开发中不太有可能写过这样子的代码——将一个变量的定义语句放到循环语句的内部,但作为一名老的VB程序员,这道题看上去太简单了,答案似乎一目了然。然而,如果真是这么简单,这名学生会费这么大功夫给我发这样一个邮件?
先不看答案,大家猜猜,结果是什么?
(空掉数行,别偷看答案!)
……
……
……
……
……
……
……
……
……
……
真实结果是:2!相信这是一个会让C#程序员大感意外的结果!难道不是每次循环开始时都新定义一个变量吗?新定义的变量应该取默认值0啊,为何会得到2?
有关“.NET面试题”的分析
为了便于分析,我将代码修改了一下,同时写了一段C#和VB.NET代码作为对比:
VB.NET代码:
- Module Module1
- Sub Main()
- For i As Integer = 1 To 10
- Dim a As Integer
- a = a + 1
- Console.WriteLine(a)
- Next
- Console.ReadKey()
- End Sub
- End Module
C#代码:
- class Program
- {
- static void Main(string[] args)
- {
- for (int i = 1; i <= 10; i++)
- {
- int a = 0; //必须初始化,否则C#编译器报错!
- a=a+1;
- Console.WriteLine(a);
- }
- Console.ReadKey();
- }
- }
运行结果是:VB.NET程序输出1到10,而C#程序输出10个“1”。
原因何在?
有的程序员可能会想到可以使用Reflector工具反汇编上述两段代码生成的程序集,看看原因到底是什么。
然而你会很失望,对比结果看不出有什么大的差异,甚至Reflector根据IL指令为VB.NET程序生成的C#代码还是错的,无法通过C#编译器的编译。
“.NET面试题”全跟踪:IL代码解读
最后一招:祭出“终极武器”——ildasm,直接阅读生成的IL指令。
在Release模式下,VB.NET程序生成的IL代码如下(我加了详细的注释,注意红色的指令):
- .method public static void Main() cil managed
- {
- .entrypoint
- .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
- // Code size 28 (0x1c)
- .maxstack 2
- //分配两个Slot,用于保存两个局部变量,对于整型变量,初值为0
- .locals init ([0] int32 a,
- [1] int32 i)
- //将“1”保存到变量”i”中
- IL_0000: ldc.i4.1
- IL_0001: stloc.1
- //将变量a的当前值装入计算堆栈
- IL_0002: ldloc.0
- //将“1” 装入计算堆栈
- IL_0003: ldc.i4.1
- //实现a=a+1,add.ovf指令从堆栈中弹出两个操作数相加,并进行溢出检查
- IL_0004: add.ovf
- //结果保存回变量a中
- IL_0005: stloc.0
- //将变量a的新值装入计算堆栈
- IL_0006: ldloc.0
- //将a的新值输出显示
- IL_0007: call void [mscorlib]System.Console::WriteLine(int32)
- //将变量i的新值装入计算堆栈
- IL_000c: ldloc.1
- //将”1”装入计算堆栈
- IL_000d: ldc.i4.1
- //实现i=i+1,循环变量自增
- IL_000e: add.ovf
- //i的新值保存到变量i中
- IL_000f: stloc.1
- //将变量i的值装入计算堆栈
- IL_0010: ldloc.1
- //将循环终值10压入计算堆栈
- IL_0011: ldc.i4.s 10
- //如果i<=10,跳到指令IL_0002处重新执行。
- IL_0013: ble.s IL_0002
- //暂停显示
- IL_0015: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
- IL_001a: pop
- //退出
- IL_001b: ret
- } // end of method Module1::Main
而C#生成的如下,为简洁起见,我只在关键语句加了注释
- .method private hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
- // Code size 32 (0x20)
- .maxstack 2
- .locals init ([0] int32 i,
- [1] int32 a)
- //i=1
- IL_0000: ldc.i4.1
- IL_0001: stloc.0
- //无条件直接跳到IL_0014处!
- IL_0002: br.s IL_0014
- //a=0
- IL_0004: ldc.i4.0
- IL_0005: stloc.1
- //a++
- IL_0006: ldloc.1
- IL_0007: ldc.i4.1
- IL_0008: add
- IL_0009: stloc.1
- //输出a的值
- IL_000a: ldloc.1
- IL_000b: call void [mscorlib]System.Console::WriteLine(int32)
- //i++
- IL_0010: ldloc.0
- IL_0011: ldc.i4.1
- IL_0012: add
- IL_0013: stloc.0
- IL_0014: ldloc.0
- //如果i<=10,跳转到IL_0004处
- IL_0015: ldc.i4.s 10
- IL_0017: ble.s IL_0004
- IL_0019: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
- IL_001e: pop
- //结束返回
- IL_001f: ret
- } // end of method Program::Main
情况很清楚了,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面试“必杀题”的“简单问题”》。
【编辑推荐】