C#如何评估包含赋值的表达式?

6

我有C/C++背景。在学习C#时,我发现了一种奇怪的交换两个变量值的方法。

int n1 = 10, n2=20;
n2 = n1 + (n1=n2)*0;

在C#中,上述两行代码确实可以交换n1n2的值。这让我感到惊讶,因为在C/C++中,结果应该是n1=n2=20
那么,C#如何评估表达式呢?在我看来,它似乎将上面的+视为函数调用。以下解释似乎是合理的。但对我来说看起来很奇怪。
  1. 首先执行(n1=n2)。因此,n1=20
  2. 然后,在n1+ (n1=n2)*0中,n1还没有变成20。它被视为一个函数参数,因此被压入堆栈并且仍然是10。因此,n2=10+0=10

3
(我认为C/C++应该是IDB/UB..) - user2864740
3
如果我在生产环境中发现类似的代码,你就要被解雇了! :) - Mitch Wheat
2
@PengZhang 【括号不会创建序列点】(https://dev59.com/Zm855IYBdhLWcg3wuG-W)。 - user2864740
2
@PengZhang 当然可以,但编译器在评估操作数 n1 并在评估 (n1=n2)*0 之前将其存储在某个地方时是自由的,只要它们的加法是执行的最后一个操作。 - user657267
1
@pengzhang 结合性和优先级控制运算符运行的顺序,而不是它们的操作数计算顺序。请确保您清楚了解这一点。 - Eric Lippert
显示剩余7条评论
3个回答

6
在C#中,子表达式按从左到右的顺序进行评估,并以此顺序产生副作用。这在C# 5规范的第7.3节中定义如下:
“表达式中的操作数从左到右进行评估。”
需要注意的是,子表达式的评估顺序与优先级(也称为运算顺序)和结合性无关。例如,在表达式A() + B() * C()中,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在这个网站他的博客上详细讨论了这个问题。

3

好的,这可能最好使用IL操作码来解释。

IL_0000:  ldc.i4.s    0A 
IL_0002:  stloc.0     // n1
IL_0003:  ldc.i4.s    14 
IL_0005:  stloc.1     // n2

第一行到第四行比较容易理解,ldc.i4将变量(4个字节的int类型)加载到栈上,而stloc.*则将栈顶的值存储起来。
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) + (..) 是一个二元运算符,按从左到右的顺序进行计算。


它只是证明了OP已经看到的行为,而没有解释为什么会这样。 - MarcinJuraszek
我认为这是符合 .NET 规范所描述的正确行为。这是一个解释,无论您是否认为它足够详尽。 - user3613916

2

阅读规范,它会告诉你真相:

7.5.1.2 Run-time evaluation of argument lists

The expressions of an argument list are always evaluated in the order they are written. Thus, the example

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++);
  }
}

produces the output

x = 0, y = 1, z = 2
x = 4, y = -1, z = 3
您可以看到,它也适用于算术运算,如果您将代码更改为:
int n1 = 10, n2=20;
n2 = (n1=n2) * 0 + n1;

现在,n1n2都等于20

谢谢。但是 n2=n1+... 不是一个函数调用,至少不是显式的,所以不应该有传递参数列表的情况吧? - Peng Zhang
1
我不相信“参数列表的表达式总是按照它们编写的顺序进行评估”隐含地适用于所有表达式。明确地解决这个问题会很好。 - user2864740

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接