"k += c += k += c;" 中是否有内联运算符的解释?

Is there an explanation for inline operators in "k += c += k += c;"?

以下操作结果的解释是什么?

k += c += k += c;

我试图理解以下代码的输出结果:

int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70

目前我正在努力理解为什么 "k" 的结果是 80。为什么分配 k=40 不起作用(实际上 Visual Studio 告诉我那个值没有在其他地方使用) ?

为什么 k 是 80 而不是 110?

如果我将操作拆分为:

k+=c;
c+=k;
k+=c;

结果是k=110。

我试图浏览 CIL,但我对生成的 CIL 的解释不是很深刻,无法获得一些细节:

 // [11 13 - 11 24]
IL_0001: ldc.i4.s     10
IL_0003: stloc.0      // k

// [12 13 - 12 24]
IL_0004: ldc.i4.s     30
IL_0006: stloc.1      // c

// [13 13 - 13 30]
IL_0007: ldloc.0      // k expect to be 10
IL_0008: ldloc.1      // c
IL_0009: ldloc.0      // k why do we need the second load?
IL_000a: ldloc.1      // c
IL_000b: add          // I expect it to be 40
IL_000c: dup          // What for?
IL_000d: stloc.0      // k - expected to be 40
IL_000e: add
IL_000f: dup          // I presume the "magic" happens here
IL_0010: stloc.1      // c = 70
IL_0011: add
IL_0012: stloc.0      // k = 80??????

a op= b;这样的操作等同于a = a op b;。赋值可以用作语句或表达式,而作为表达式它会产生指定的值。你的陈述...

k += c += k += c;

...可以,因为赋值运算符是右结合的,也可以写成

k += (c += (k += c));

或(展开)

k =  k +  (c = c +  (k = k  + c));
     10    →   30    →   10 → 30   // operand evaluation order is from left to right
      |         |        ↓    ↓
      |         ↓   40 ← 10 + 30   // operator evaluation
      ↓   70 ← 30 + 40
80 ← 10 + 70

在整个评估过程中,使用相关变量的旧值。对于 k 的值尤其如此(请参阅我对下面的 IL 的评论和 link Wai Ha Lee 提供的)。因此,您得到的不是 70 + 40(k 的新值)= 110,而是 70 + 10(k 的旧值)= 80.

重点是(根据本页的 C# spec) "Operands in an expression are evaluated from left to right" (the operands are the variables c and k in our case). This is independent of the operator precedence and associativity which in this case dictate an execution order from right to left. (See comments to Eric Lippert's )。


现在让我们看看 IL。 IL 假定基于堆栈的虚拟机,即它不使用寄存器。

IL_0007: ldloc.0      // k (is 10)
IL_0008: ldloc.1      // c (is 30)
IL_0009: ldloc.0      // k (is 10)
IL_000a: ldloc.1      // c (is 30)

堆栈现在看起来像这样(从左到右;堆栈顶部在右边)

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0      // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1      // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0      // k <-- 80

请注意 IL_000c: dupIL_000d: stloc.0,即对 k 的第一个赋值,可以优化掉。这可能是在将 IL 转换为机器代码时通过抖动对变量完成的。

另请注意,计算所需的所有值要么在进行任何赋值之前被压入堆栈,要么根据这些值计算得出。分配的值(由 stloc)在此评估期间永远不会重复使用。 stloc 弹出栈顶。


以下控制台测试的输出是(Release 优化模式)

evaluating k (10)
evaluating c (30)
evaluating k (10)
evaluating c (30)
40 assigned to k
70 assigned to c
80 assigned to k

private static int _k = 10;
public static int k
{
    get { Console.WriteLine($"evaluating k ({_k})"); return _k; }
    set { Console.WriteLine($"{value} assigned to k"); _k = value; }
}

private static int _c = 30;
public static int c
{
    get { Console.WriteLine($"evaluating c ({_c})"); return _c; }
    set { Console.WriteLine($"{value} assigned to c"); _c = value; }
}

public static void Test()
{
    k += c += k += c;
}

归结为:第一个 += 是应用于原始 k 还是应用于计算得更靠右的值?

答案是虽然赋值是从右到左绑定的,但操作仍然是从左到右进行的。

所以最左边的+=正在执行10 += 70

首先,Henk 和 Olivier 的回答是正确的;我想用一种稍微不同的方式来解释它。具体来说,我想谈谈你提出的这一点。你有这组语句:

int k = 10;
int c = 30;
k += c += k += c;

然后您错误地认为这应该给出与这组语句相同的结果:

int k = 10;
int c = 30;
k += c;
c += k;
k += c;

看看你是怎么弄错的,以及如何做对的,这对你很有帮助。正确的分解方式是这样的

首先重写最外层的+=

k = k + (c += k += c);

其次,重写最外层的+。 我希望你同意 x = y + z 必须始终与 "evaluate y to a temporary, evaluate z to a temporary, sum the temporaries, assign the sum to x" 相同。所以让我们把它说得非常明确:

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;

确保清楚,因为这是你弄错的步骤。将复杂的操作分解为更简单的操作时,您必须确保缓慢而小心地并且不要跳过步骤。跳过步骤是我们犯错误的地方。

好的,现在再次缓慢而仔细地分解对 t2 的分配。

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;

分配给 t2 的值与分配给 c 的值相同,所以假设:

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;

太棒了。现在分解第二行:

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

很好,我们正在取得进展。分解对 t4 的分配:

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

现在分解第三行:

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

现在我们可以看看整个事情了:

int k = 10;  // 10
int c = 30;  // 30
int t1 = k;  // 10
int t3 = c;  // 30
int t4 = k + c; // 40
k = t4;         // 40
int t2 = t3 + t4; // 70
c = t2;           // 70
k = t1 + t2;      // 80

所以当我们完成后,k 是 80,c 是 70。

现在让我们看看这是如何在 IL 中实现的:

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3

现在这有点棘手:

int t4 = k + c; 
k = t4;         
  is implemented as
ldloc.0      // load k
ldloc.1      // load c
add          // sum them to stack slot 3
dup          // t4 is stack slot 3, and is now equal to the sum
stloc.0      // k is now also equal to the sum

我们本可以将上述实现为

ldloc.0      // load k
ldloc.1      // load c
add          // sum them
stloc.0      // k is now equal to the sum
ldloc.0      // t4 is now equal to k

但是我们使用了"dup"技巧,因为它使代码更短并且更容易抖动,我们得到了相同的结果。 一般来说,C# 代码生成器会尝试尽可能多地在堆栈上保留临时变量 "ephemeral"。 如果您发现使用较少的临时变量更容易遵循 IL,请转为优化 关闭,代码生成器将不那么激进。

我们现在必须使用相同的技巧来获取 c:

int t2 = t3 + t4; // 70
c = t2;           // 70
  is implemented as:
add          // t3 and t4 are the top of the stack.
dup          
stloc.1      // again, we do the dup trick to get the sum in 
             // both c and t2, which is stack slot 2.

最后:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.

因为我们不需要其他任何东西的总和,所以我们不会复制它。堆栈现在是空的,我们在语句的末尾。

这个故事的寓意是:当你试图理解一个复杂的程序时,总是一次分解一个操作。不要走捷径;他们会让你误入歧途。

你可以通过数数来解决这个问题。

a = k += c += k += c

有两个c和两个k所以

a = 2c + 2k

并且,由于语言的运算符,k 也等于 2c + 2k

这适用于这种类型链中的任意变量组合:

a = r += r += r += m += n += m

所以

a = 2m + n + 3r

r将相等。

您只需计算最左边的赋值即可计算出其他数字的值。所以 m 等于 2m + nn 等于 n + m

这表明 k += c += k += c;k += c; c += k; k += c; 不同,因此您会得到不同的答案。

评论中的一些人似乎担心您可能会尝试从这个快捷方式过度概括到所有可能的加法类型。因此,我将明确表示此快捷方式仅适用于这种情况,即将内置数字类型的加法赋值链接在一起。如果您添加其他运算符,它不会(必然)起作用,例如()+,或者如果您调用函数或者如果您覆盖了 +=,或者如果您使用的不是基本数字类型。 这只是为了帮助解决问题中的特定情况

对于这种链式赋值,你必须从最右边开始赋值。你必须分配和计算并将它分配到左侧,并一直这样做到最后(最左边的分配),当然它被计算为 k = 80。

简单的答案:用值替换变量,你知道了:

int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!

我用 gcc 和 pgcc 尝试了示例并得到了 110。我检查了它们生成的 IR,编译器确实将 expr 扩展为:

k = 10;
c = 30;
k = c+k;
c = c+k;
k = c+k;

我觉得很合理。