- 注册时间
- 2006-6-19
- 最后登录
- 2010-1-23
⑥精研
- 积分
- 2223
|
作者: RednaxelaFX
日期: 2007-09-28
原文地址: http://bestimmung.iblog.com/post/222303/424207
最近周围有好多同学都在看各种面试"宝典",其中最多的还是程序方面的咯.然而这些"宝典"的质量参差不齐,有些很明显是错的还写在上面,让人颇为无奈.嗯这两天也被问到一些问题,今天就先对C#和Java中带副作用的表达式,主要是前/后自增/自减运算符的特性的问题做点笔记.正好之前执竞也问过我几乎一样的问题,还有点印象.希望读者通过本文能记住一点: 千万不要写出 i = i++; 这样的语句!!
使用C#或Java编程,意味着要使用imperative programming style来写代码.虽然C#中lambda表达式给出了一点functional programming的味道,但它到底能对C#的整体使用状况有多少改变,还需要观察.
Imperative programming style,即命令式编程,最大的特点是程序是按命令的顺序来执行的,"表达式"中可以含有副作用,而这些副作用有先后依赖关系.同一"表达式"被多次使用时不一定会返回同样的结果.
Functional Programming style,即函数式编程,则不允许副作用,因此一个表达式无论在什么时候执行都一定会得到一样的结果.纯函数式编程语言中也没有"变量",因为所谓"变量"也只能赋初值而不能在后续运行中改变其值.这样让表达式的语义更加清晰,而且也便于lazy evaluation和并发执行.
在C#/Java中,表达式里有方法调用或一些运算符都有可能带来副作用.有副作用的运算符主要是++和--两组.这里就这两组运算符的特性展开讨论.只对语言表象感兴趣的可以只读本文的前半部分及其总结,而对运行机制感兴趣的请耐心读完IL(中间语言)分析的部分.
--------------------------------------------------------------------------------
由于本文里有不少代码,而iblog这里发代码实在太难看了,干脆就不把内容放在页面上了.本文的完整内容请看附件: http://bestimmung.iblog.com/get/ ... de_effect_notes.txt
因为IE/FF等浏览器不会对txt格式的纯文本文件做自动换行,建议将文件下载之后再看...
--------------------------------------------------------------------------------
在论坛能用UBB代码还是比iblog那边舒服些...顺便把一部分代码帖出来吧:
-------------------------------------------------------------------
C# code:
-------------------------------------------------------------------
- public class TestIncrCS
- {
- public static void Main( string[ ] args )
- {
- Foo1();
- Foo2();
- Foo3();
- }
- public static void Foo1( )
- {
- int i = 0;
- i = i++;
- // i == 0
- }
- public static void Foo2( )
- {
- int i = 1;
- i = ++i;
- // i == 2
- }
- public static void Foo3( )
- {
- int i = 2;
- i = i++ + i++ + ++i;
- // i == 10 ← 3 + 4 + 3
- }
- }
复制代码
-------------------------------------------------------------------
Java code:
-------------------------------------------------------------------
- public class TestIncrJava
- {
- public static void main( String[ ] args ) {
- foo1();
- foo2();
- foo3();
- }
- public static void foo1( ) {
- int i = 0;
- i = i++;
- // i == 0
- }
- public static void foo2( ) {
- int i = 1;
- i = ++i;
- // i == 2
- }
- public static void foo3( ) {
- int i = 2;
- i = i++ + i++ + ++i;
- // i == 10 ← 3 + 4 + 3
- }
- }
复制代码
以C#中的Foo1()与Java中的foo1()为例详细解释一下.其它的几个方法,请读者自行思考.
C#的Foo1()中,
.maxstack 3 // 该方法的栈最多使用3个单元的空间
.locals init (int32 V_0) // 该方法的局部变量声明,这里是一个Int32类型的局部变量,叫做V_0,对应于原C#代码中的i
IL_0000: nop // 空指令
IL_0001: ldc.i4.0 // 装载一个32位的整形常量,值为0.将这个数压到栈上.
IL_0002: stloc.0 // 将栈顶的值弹出,放入局部变量区中下标为0的单元中,也就是赋值给V_0
IL_0003: ldloc.0 // 从局部变量区中下标为0的单元取出值,并压到栈顶
IL_0004: dup // 将栈顶的值复制一份,同样压入栈中
IL_0005: ldc.i4.1 // 装载一个32位的整形常量,值为1.将这个数压到栈上.
IL_0006: add // 从栈顶弹出两个值,将它们相加,然后压回到栈中
IL_0007: stloc.0 // 将栈顶的值弹出,放入局部变量区中下标为0的单元中,也就是赋值给V_0
IL_0008: stloc.0 // 将栈顶的值弹出,放入局部变量区中下标为0的单元中,也就是赋值给V_0
IL_0009: ret // 方法返回,无返回值
其中,IL_0001与IL_0002是由int i = 0;编译而来.IL_0009是方法结束的标示.中间的就是i = i++;编译而来的MSIL.
IL_0003,IL_0004分别将i等于0的值压入栈中.IL_0005将常量1压入栈中,IL_0006让栈顶的两值相加,IL_0007将栈顶的值弹出,存到变量中.也就是说,IL_0004-IL_0007完成了i++.
到IL_0008执行前,栈顶还留有IL_0003压入的那个0.而正是这个0,将i++的效果冲掉了,将i的值重新赋为0.
Java的foo1()中,
0: iconst_0 // 装载一个32位的整形常量,值为0.将这个数压到栈上.
1: istore_0 // 将栈顶的值弹出,放入局部变量区中下标为0的单元中,也就是赋值给i
2: iload_0 // 从局部变量区中下标为0的单元取出值,并压到栈顶
3: iinc 0, 1 // 将局部变量区中下标为0的单元自增1,直接保存在原单元中
6: istore_0 // 将栈顶的值弹出,放入局部变量区中下标为0的单元中,也就是赋值给i
7: return // 方法返回,无返回值
Java版本与C#版本其实是完全对应的,所以不用详细讲解了.
指令0对应IL_0001,
指令1对应IL_0002,
指令2对应IL_0003,
指令3对应IL_0004-IL_0007
指令6对应IL_0008
指令7对应IL_0009
===============================================================
对"栈"这一概念不熟悉的读者可能会觉得奇怪,i等于0明明是先入栈的,为什么反而会将在后的i++的效果冲掉了呢?
这是因为栈是一种LIFO(先入后出)的数据结构,所以进入栈的顺序与退出栈的顺序正好的相反的.所以当我们看到一对load/store在中间i++代码的"外面"时,我们就已经可以知道最终的运算结果了.
如果因为先入为主,只知道以基于寄存器的模式去思考的话,要理解这基于栈的MSIL/JVM指令集确实是由点别扭... |
|