在编程实践中,当人们开始变得富有创造性时,问题也随之产生。基本上,前缀(如++i或--i)表示在变量使用前增加或减少其值,因此变量立即具有新值:
C# i = 10;
x = ++i + 5;
可以这样理解:
C# i = 10;
i = i + 1;
x = i + 5;
对于--版本也类似:
C# i = 10;
x = --i + 5;
可以这样理解:
C# i = 10;
i = i - 1;
x = i + 5;
后缀版本(i++或i--)执行相同的操作,但在变量使用后进行:
C# i = 10;
x = i++ + 5;
可以这样理解:
C# i = 10;
x = i + 5;
i = i + 1;
同样地:
C# i = 10;
x = i-- + 5;
可以这样理解:
C# i = 10;
x = i + 5;
i = i - 1;
就这么简单吗?是的,如果以简单的方式使用它 - 例如作为数组索引器:
C# x = myArray[index++];
或者作为循环增量:
C# for(i = 0; i < 10; i++)
{
WriteLine(myArray[i]);
}
但之后,就进入了混乱和痛苦的世界!例如,这将i的值设置为什么:
C# int i, j;
i = 10;
for(j = 0; j < 5; j++)
{
i = i++;
}
答案是:不变。 i保持在10。为什么?想象一下如果展开它:
C# int i = 10;
i = i++;
如果在C#中写这个,那么IL看起来像这样:
MSIL
.line 14,14 : 13,24 ''
IL_0001:
ldc.i4.s
10
Push a constant value '10' to the stack,
4
byte
integer,
IL_0003:
stloc.0
Pop
the top of the stack into local number
0
.line 15,15 : 13,21 ''
IL_0004:
ldloc.0
Push local number
0
to the stack
IL_0005:
dup
Duplicate the top of the stack
IL_0006:
ldc.i4.1
Push a constant value '1',
4
byte
integer
IL_0007:
add
Pop
the top two stack items,
add
them,
and
push the result
IL_0008:
stloc.0
Store the top of the stack in local number
0
IL_0009:
stloc.0
Store the top of the stack in local number
0
什么?在扩展的C#代码中,这变成了:
C# int i = 10;
int j = i;
int k = j;
k = k + 1;
i = k;
i = j;
这相当奇怪...因为可以在不影响结果的情况下丢弃中间的三行。它不应该这样做,对吧?是的。是的,它应该: i++是一个后缀操作:它说,“记住i的值,然后将i增加1,然后返回记住的值”。所以告诉编译器做的是忽略增量的结果,通过用开始的值覆盖它。有趣的是,如果在Visual Studio C++编译器中尝试...它不会...因为它以不同的方式处理!所以,现在有第一个原因为什么当开始使用增量和减量运算符时要谨慎:它实际上是一个副作用,一整行代码插入到行中,如果不仔细思考,它就不会做认为的事情。
C# int i1 = i; i1 = i1 + 1; i = i
问题出现在开始在同一行混合操作时:
C# i = 10; x = ++i + i++;
问题是语言规范没有定义前缀和后缀操作应该何时发生,甚至没有定义操作符的顺序,考虑到操作符优先级规则。
优先级和求值顺序:
MSDN写道: 只有顺序评估(,)、逻辑与(&&)、逻辑或(||)、条件表达式(?:)和函数调用运算符构成序列点,因此保证其操作数的特定求值顺序。
所以编译器作者可以评估这个表达式:
C# a = b * c + d * e;
通过首先计算b * c,然后计算d * e,或者首先计算d * e然后计算b * c,因为它对计算结果没有影响。
除非开始玩前缀和后缀增量运算符 - 然后结果会改变。
这意味着它完全是实现特定的,将得到什么结果:i的值应该总是相同的:
12
但x的值可能会因编译器(在一定程度上取决于目标处理器 - ARM例如有内置的前缀和后缀增量和减量到其“机器代码”LOAD操作,所以一个高效的编译器很可能会直接使用它们)而异,应该执行为:
C# i = 10;
i = i + 1;
x = i + i;
i = i + 1;
结果为22或为:
C# i = 10;
i1 = i;
i = i + 1;
x = i1 + i;
i = i + 1;
结果为21或为:
C# i = 10;
i1 = i;
i = i + 1;
x = i + i1;
i = i + 1;
这也给出了21,通过不同的路线。并记住编译器不必以从左到右的顺序评估“+”的两个操作数,所以它甚至可能给出一些非常奇怪和意想不到的结果!像23...
更糟的是:求值顺序可能会在编译器版本之间改变(因为它没有定义)或者作为优化的结果:所以调试中有效的东西在发布中失败了!
所以避免组合它们:在“简单表达式”中使用它们,例如每次循环时递增数组索引,但不要花哨,否则代码可能会以有趣的方式失败...
有趣的点
这篇文章最初是作为一个问题的答案写的 - 但在需要它的时候从未找到过它,并且觉得它需要被保存以供后人参考。因此它被更新了一点,扩展了,发布为一篇文章,并且得到了更高的可见性。