当前位置: 首页 > news >正文

一道可以成为.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,用的人多,也就成了标准。事实上,实践是起决定性作用的。

相关文章:

  • Linux 简单的网络配置练习一
  • 解决Ubuntu 9.04 耳机有声音但外放无声问题
  • IPv4和IPv6比特转发率和包转发率的关系
  • [LeetCode]-Pascal's Triangle III 杨辉三角问题
  • 令狐冲和TCP/IP协议的第三层协议的关系
  • [LeetCode]-Spiral Matrix III 螺旋矩阵
  • 蓝牙3.0+HS规范正式公布 携手802.11大提速
  • [LeeCode]-Divide Two Integers 不用乘除的除法运算
  • 浏览器之父卷土重来 开发新浏览RockMelt
  • Singleton Pattern 单例模式
  • 浏览器也能当操作系统!——3款中文浏览器操作系统体验评测
  • Linux进程管理中的hash
  • 浏览器真的能“永不假死”?——六款主流浏览器防假死功能测试
  • [九度—剑指offer]—二维数组查找
  • 人人都能当“苍天哥” 手把手教你制作游戏视频
  • Docker下部署自己的LNMP工作环境
  • download使用浅析
  • exif信息对照
  • iOS 颜色设置看我就够了
  • JDK9: 集成 Jshell 和 Maven 项目.
  • Linux快速复制或删除大量小文件
  • spring + angular 实现导出excel
  • SQLServer插入数据
  • swift基础之_对象 实例方法 对象方法。
  • WinRAR存在严重的安全漏洞影响5亿用户
  • Work@Alibaba 阿里巴巴的企业应用构建之路
  • 关于Java中分层中遇到的一些问题
  • 关于字符编码你应该知道的事情
  • 罗辑思维在全链路压测方面的实践和工作笔记
  • 前端设计模式
  • 巧用 TypeScript (一)
  • 算法-插入排序
  • 突破自己的技术思维
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • ​创新驱动,边缘计算领袖:亚马逊云科技海外服务器服务再进化
  • ​渐进式Web应用PWA的未来
  • ​决定德拉瓦州地区版图的关键历史事件
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • ()、[]、{}、(())、[[]]命令替换
  • (cljs/run-at (JSVM. :browser) 搭建刚好可用的开发环境!)
  • (pytorch进阶之路)扩散概率模型
  • (webRTC、RecordRTC):navigator.mediaDevices undefined
  • (二)springcloud实战之config配置中心
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (九)One-Wire总线-DS18B20
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理 第13章 项目资源管理(七)
  • (排序详解之 堆排序)
  • (七)微服务分布式云架构spring cloud - common-service 项目构建过程
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (五)网络优化与超参数选择--九五小庞
  • (译) 函数式 JS #1:简介
  • (译) 理解 Elixir 中的宏 Macro, 第四部分:深入化