是否在“ k + = c + = k + = c;”中对内联运算符进行了解释?

时间:2019-02-13 16:14:07

标签: c# cil compound-assignment

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

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??????

7 个答案:

答案 0 :(得分:102)

类似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“表达式中的操作数是从左到右计算的” (操作数是变量c和{ {1}})。这与运算符优先级和关联性无关,在这种情况下,运算符优先级和关联性指示了从右到左的执行顺序。 (请参见本页上对Eric Lippert的answer的评论)。


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

k

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

  

10 30 10 30

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 40

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

10 30 40 40

IL_000c: dup
  

10 30 40

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

10 70

IL_000e: add
  

10 70 70

IL_000f: dup
  

10 70

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

80

IL_0011: add

请注意,IL_0012: stloc.0 // k <-- 80 IL_000c: dup(即对IL_000d: stloc.0的第一次分配)可以进行优化。在将IL转换为机器代码时,可能通过抖动对变量进行了处理。

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


以下控制台测试的输出为(stloc模式,且启用了优化)

  

计算k(10)
  评估c(30)
  评估k(10)
  评估c(30)
  40个分配给k
  70个分配给c
  80分配给k

Release

答案 1 :(得分:24)

首先,亨克和奥利维尔的答案是正确的;我想用稍微不同的方式来解释它。具体来说,我想解决您提出的这一点。您有以下这组语句:

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必须始终与“将y评估为一个临时值,将z评估为一个临时值,将临时项求和,将总和分配给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#代码生成器会尝试将临时信息尽可能地保持在临时信息上。如果您发现使用较少的临时信息来遵循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.

由于我们不需要其他任何东西的总和,因此我们不会将其重复。堆栈现在是空的,我们在语句的结尾。

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

答案 2 :(得分:14)

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

答案是,尽管分配从右到左绑定,但操作仍然从左到右进行。

因此最左边的+=正在执行10 += 70

答案 3 :(得分:0)

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

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

在我看来合理。

答案 4 :(得分:-1)

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

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 + n,而n等于n + m

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

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

答案 5 :(得分:-1)

对于这种链分配,您必须从最右边开始分配值。您必须分配并计算并将其分配到左侧,然后一直进行到最终(最左侧的分配),请确保将其计算为k = 80。

答案 6 :(得分:-1)

简单的答案:将vars替换为您知道的值

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