幻想森林

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 3215|回复: 6

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

[复制链接]

8

主题

215

帖子

2223

积分

⑥精研

积分
2223
发表于 2007-9-28 14:11:45 | 显示全部楼层 |阅读模式
作者: 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:
-------------------------------------------------------------------
  1. public class TestIncrCS
  2. {
  3.     public static void Main( string[ ] args )
  4.     {
  5.         Foo1();
  6.         Foo2();
  7.         Foo3();
  8.     }
  9.     public static void Foo1( )
  10.     {
  11.         int i = 0;
  12.         i = i++;
  13.     // i == 0
  14.     }
  15.     public static void Foo2( )
  16.     {
  17.         int i = 1;
  18.         i = ++i;
  19.     // i == 2
  20.     }
  21.     public static void Foo3( )
  22.     {
  23.         int i = 2;
  24.         i = i++ + i++ + ++i;
  25.     // i == 10 ← 3 + 4 + 3
  26.     }
  27. }
复制代码


-------------------------------------------------------------------
Java code:
-------------------------------------------------------------------
  1. public class TestIncrJava
  2. {
  3.     public static void main( String[ ] args ) {
  4.         foo1();
  5.         foo2();
  6.         foo3();
  7.     }
  8.     public static void foo1( ) {
  9.         int i = 0;
  10.         i = i++;
  11.     // i == 0
  12.     }
  13.     public static void foo2( ) {
  14.         int i = 1;
  15.         i = ++i;
  16.     // i == 2
  17.     }
  18.     public static void foo3( ) {
  19.         int i = 2;
  20.         i = i++ + i++ + ++i;
  21.     // i == 10 ← 3 + 4 + 3
  22.     }
  23. }
复制代码


以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指令集确实是由点别扭...
回复

使用道具 举报

136

主题

1751

帖子

548

积分

版主

Rank: 7Rank: 7Rank: 7

积分
548
发表于 2007-9-28 15:02:17 | 显示全部楼层
写得不错,详尽又浅显易懂,我这样的asm盲都不成问题。
试了下,d语言中也是这样。看来编译器处理i++和++i时是不一样的。
え~え~お!!!
回复 支持 反对

使用道具 举报

8

主题

215

帖子

2223

积分

⑥精研

积分
2223
 楼主| 发表于 2007-9-28 15:39:36 | 显示全部楼层
嗯,上传那附件的时候忘写了,VC9的C++编译器展现的行为有那么点lazy evaluation的味道.是不是真的lazy evalution得查点资料才知道了...
相反,C#/Java的语言规范里都规定了不使用lazy evaluation.
回复 支持 反对

使用道具 举报

19

主题

842

帖子

1万

积分

⑧专业

絕望青年,一起增高吧

积分
13676
发表于 2007-9-28 18:20:18 | 显示全部楼层
LZ的功力果然深厚。。。
雖然一般理解i++和++i在入門時相信已經有不少解釋
但解剖到asm地步,相信大家也不多作吧。。XD

為著彼岸,便要與之妥協 但為著彼岸,更不能與之妥協

回复 支持 反对

使用道具 举报

50

主题

742

帖子

402

积分

版主

自定义头衔

Rank: 7Rank: 7Rank: 7

积分
402
发表于 2007-9-28 19:36:38 | 显示全部楼层
尽量少用有歧义的,
++i; ++j; i = j; 而不是混合到一个式子

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

以上个人观点,副作用自己也不是很懂,也不太想去知道太多^^
比较关心的话,那就是左值、右值的理解了……
[s:8]
Style-C
回复 支持 反对

使用道具 举报

8

主题

215

帖子

2223

积分

⑥精研

积分
2223
 楼主| 发表于 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)++只能是右值不同.
回复 支持 反对

使用道具 举报

50

主题

742

帖子

402

积分

版主

自定义头衔

Rank: 7Rank: 7Rank: 7

积分
402
发表于 2007-9-28 20:10:05 | 显示全部楼层
...? 这样写的话括号就没意义了.*ptr++与*(ptr++)一样是左值,而与(*ptr)++只能是右值不同.
那就算是让编译器多花点功夫巴XD~

偶说左值其实只是另外的话题了XD
Style-C
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|幻想森林

GMT+8, 2024-4-29 18:32 , Processed in 0.021961 second(s), 20 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表