一道可以成为.NET面试“必杀题”的“简单问题”

一道可以成为.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?

 

为了便于分析,我将代码修改了一下,同时写了一段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程序输出110,而C#程序输出10个“1”。

原因何在?

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

 

 

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

 

最后一招:祭出“终极武器”——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+1add.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_0004IL_0005),而这两个语句又在循环体内,每次循环开始a都回到初值0,因此,输出10个“1”。

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

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

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

 

注:

有关IL编程的基础知识,请参考本人拙著《.NET 2.0面向对象编程揭秘》。另外,使用MSDN,可以查询到MSIL中所有指令的技术特性。我将在新著中也会对IL编程作介绍。

 

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

看了些网友的回复,感到我可能表达得不够清楚,他们可能并没有了解本文要表达的意思:

1 之所以说这道题“必杀”是因为它考核的是两种语言编译器的具体实现方式,过于偏了,如果作为面试题被习惯了C#的程序员看到,估计10人里面有9人会弄错。这说明“思维定式”有时会让人犯错误的,看问题和分析问题要尽量“客观”而不要“主观”,不要轻易地得出结论。

2 通篇我并未说:掌握语言特性需要分析其具体编译实现入手,而只是说:想弄明白一些东西,得去分析编译器生成出来的最终指令,因此下点功夫掌握MSIL并非无用,强调要重视对调试能力的培养。

3 有一名网友提到了“标准”问题,“标准”要为大家所遵循才有用,如果是“标准”,但实际上大家各行其事,“标准”就是一纸空文,这种事在IT业还少吗?因此,还是要根据具体情况进行具体分析,象TCP/IP,用的人多,也就成了标准。事实上,实践是起决定性作用的。

 

http://blog.csdn.net/bitfan/archive/2009/08/14/4445284.aspx

加支付宝好友偷能量挖...


评论(0)网络
阅读(113)喜欢(0)Asp.Net/C#/WCF