rednaxela 发表于 2007-9-28 14:11:45

[转帖] 转自己刚写一文,[C#][Java] 关于带副作用的表达式的一

作者: 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/222303/256540-%5BCShapr%5D%5BJava%5D%5BC%2B%2B%5Dside_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()中,

.maxstack3             // 该方法的栈最多使用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指令集确实是由点别扭...

shawind 发表于 2007-9-28 15:02:17

写得不错,详尽又浅显易懂,我这样的asm盲都不成问题。
试了下,d语言中也是这样。看来编译器处理i++和++i时是不一样的。

rednaxela 发表于 2007-9-28 15:39:36

嗯,上传那附件的时候忘写了,VC9的C++编译器展现的行为有那么点lazy evaluation的味道.是不是真的lazy evalution得查点资料才知道了...
相反,C#/Java的语言规范里都规定了不使用lazy evaluation.

coolpay64 发表于 2007-9-28 18:20:18

LZ的功力果然深厚。。。
雖然一般理解i++和++i在入門時相信已經有不少解釋
但解剖到asm地步,相信大家也不多作吧。。XD

lw 发表于 2007-9-28 19:36:38

尽量少用有歧义的,
++i; ++j; i = j; 而不是混合到一个式子

尽量用惯性用法,
*(ptr)++ = (char)x;

以上个人观点,副作用自己也不是很懂,也不太想去知道太多^^
比较关心的话,那就是左值、右值的理解了……

rednaxela 发表于 2007-9-28 19:52:04

引用第4楼lw于2007-09-28 19:36发表的:
尽量少用有歧义的,
++i; ++j; i = j; 而不是混合到一个式子
嗯,写了那么多,想说的就是这个而已.代码的清晰性是第一位的.在可以保证正确运行的基础上,有必要的时候才做会破坏代码可阅读性的优化.

引用第4楼lw于2007-09-28 19:36发表的:
尽量用惯性用法,
*(ptr)++ = (char)x;
...? 这样写的话括号就没意义了.*ptr++与*(ptr++)一样是左值,而与(*ptr)++只能是右值不同.

lw 发表于 2007-9-28 20:10:05


...? 这样写的话括号就没意义了.*ptr++与*(ptr++)一样是左值,而与(*ptr)++只能是右值不同.

那就算是让编译器多花点功夫巴XD~

偶说左值其实只是另外的话题了XD
页: [1]
查看完整版本: [转帖] 转自己刚写一文,[C#][Java] 关于带副作用的表达式的一