我有C/c++背景。我在c#中遇到了一种奇怪的交换两个值的方式。
int n1 = 10, n2=20;
n2 = n1 + (n1=n2)*0;
在c#中,上述两行确实在n1
和n2
之间交换值。这对我来说是一个惊喜,因为在C/c++中,结果应该是n1=n2=20
。
那么,c#如何计算表达式呢?在我看来,上面的+
被视为function calling
。下面的解释似乎是合理的。但对我来说似乎有点奇怪。
- 首先执行
(n1=n2)
。这就是n1=20
。 - 则
n1+ (n1=n2)*0
中的n1
尚未为20。它被视为一个函数参数,因此被压入堆栈,仍然是10。因此,n2=10+0=10
.
在c#中,子表达式是按照从左到右的顺序求值的,副作用也是按照这个顺序产生的。这在c# 5规范第7.3节中定义:
重要的是要认识到子表达式求值的顺序与优先级(即操作顺序)和结合性无关。例如,在像表达式中的操作数从左到右求值。
A() + B() * C()
. 1这样的表达式中。c#中的求值顺序始终是A()
, B()
, C()
。我对C/c++的有限理解是,这个顺序是编译器实现的细节。
在您的示例中,对+的左操作数求第一个n1(10)。然后计算(n1=n2)
。这样做的结果是n2(20)的值,并且产生了赋值给n1的副作用。N1现在是20。然后20 * 0的乘法得到0。然后计算10 + 0,并将结果(10)赋值给n2。因此,最后期望的状态是n1 = 20, n2 = 10。
Eric Lippert在本网站和他的博客上详细讨论了这个问题。
好的,所以这可能是最好的解释使用IL操作码。
IL_0000: ldc.i4.s 0A
IL_0002: stloc.0 // n1
IL_0003: ldc.i4.s 14
IL_0005: stloc.1 // n2
前4行有点不言自明的ldc。I4在stloc时只将变量(大小为4的int)加载到堆栈中。*将值存储在栈顶
IL_0006: ldloc.0 // n1
IL_0007: ldloc.1 // n2
IL_0008: stloc.0 // n1
IL_0009: stloc.1 // n2
这几行基本上就是你所描述的。每个值只在堆栈中加载,n1在n2之前,然后存储,但是n1在n2之前存储(因此交换)
我相信这是。net规范中描述的正确行为。
mikez也添加了更多的细节,帮助我找到答案,但我相信答案是真正解释在7.3.1
当操作数出现在两个具有相同优先级的操作符之间时,操作符的结合性控制操作的执行顺序:
除赋值操作符和空合并操作符外,所有二进制操作符都是左结合的,即从左向右执行操作。例如,x + y + z的求值为(x + y) + z。
赋值操作符、空合并操作符和条件操作符(?:)是右结合的,这意味着操作从右向左执行。例如,x = y = z被求值为x = (y = z)。可以使用括号来控制优先级和结合性。例如,x + y * z首先将y乘以z,然后将结果添加到x,但是(x + y) * z首先将x和y相加,然后将结果乘以z。
这里重要的是运算的求值顺序所以实际求值的是
n2 = (n1) + ((n1=n2)*0)
其中(n1) +(…)由二进制运算符从左到右求值
阅读说明书,它会告诉你真相:
7.5.1.2实参列表 的运行时求值实参列表的表达式总是按顺序求值它们是写下来的。因此,示例
class Test { static void F(int x, int y = -1, int z = -2) { System.Console.WriteLine("x = {0}, y = {1}, z = {2}", x, y, z); } static void Main() { int i = 0; F(i++, i++, i++); F(z: i++, x: i++); } }
产生输出
x = 0, y = 1, z = 2 x = 4, y = -1, z = 3
你可以看到它也适用于算术运算,如果你改变你的代码:
int n1 = 10, n2=20;
n2 = (n1=n2) * 0 + n1;
现在,n1
和n2
等于20
。