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

And*_*rov 88 c# cil compound-assignment

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

k += c += k += c;
Run Code Online (Sandbox Code Playgroud)

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

int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70
Run Code Online (Sandbox Code Playgroud)

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

为什么k 80而不是110?

如果我将操作拆分为:

k+=c;
c+=k;
k+=c;
Run Code Online (Sandbox Code Playgroud)

结果是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??????
Run Code Online (Sandbox Code Playgroud)

Oli*_*bes 103

类似的操作a op= b;相当于a = a op b;.赋值可以用作语句或表达式,而作为表达式,它可以生成赋值.你的陈述......

k += c += k += c;
Run Code Online (Sandbox Code Playgroud)

...因为赋值运算符是右关联的,所以也可以写成

k += (c += (k += c));
Run Code Online (Sandbox Code Playgroud)

或(扩展)

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
Run Code Online (Sandbox Code Playgroud)

在整个评估期间,使用所涉及变量的旧值.对于价值来说尤其如此k(参见我对以下IL的评论以及Wai Ha Lee提供的链接).因此,你没有得到70 + 40(新值k)= 110,但70 + 10(旧值k)= 80.

关键是(根据C#规范)"表达式中的操作数从左到右进行评估"(操作数是我们案例中的变量).这与运算符优先级无关,在这种情况下,运算符优先级指示从右到左的执行顺序.(请参阅Eric Lippert 在此页面上的回答).


现在让我们来看看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)
Run Code Online (Sandbox Code Playgroud)

堆栈现在看起来像这样(从左到右;堆栈顶部是正确的)

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back
Run Code Online (Sandbox Code Playgroud)

10 30 40

IL_000c: dup
Run Code Online (Sandbox Code Playgroud)

10 30 40 40

IL_000d: stloc.0      // k <-- 40
Run Code Online (Sandbox Code Playgroud)

10 30 40

IL_000e: add
Run Code Online (Sandbox Code Playgroud)

10 70

IL_000f: dup
Run Code Online (Sandbox Code Playgroud)

10 70 70

IL_0010: stloc.1      // c <-- 70
Run Code Online (Sandbox Code Playgroud)

10 70

IL_0011: add
Run Code Online (Sandbox Code Playgroud)

80

IL_0012: stloc.0      // k <-- 80
Run Code Online (Sandbox Code Playgroud)

需要注意的是c,k即第一分配IL_000c: dup ,可能会被优化掉.在将IL转换为机器代码时,可能会通过抖动对变量进行此操作.

另请注意,计算所需的所有值要么在进行任何赋值之前被推入堆栈,要么从这些值计算得出.IL_000d: stloc.0在此评估期间,永远不会重复使用分配的值(by ).k弹出堆栈的顶部.


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

评估k(10)
评估c(30)
评估k(10)
评估c(30)
40分配给k
70分配给c
80分配给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;
}
Run Code Online (Sandbox Code Playgroud)

  • 实际上,如果`k`是本地的,那么如果优化已经开始,则几乎肯定会删除死存储,如果不是,则保留死存储.一个有趣的问题是,如果`k`是一个字段,属性,数组插槽等,是否允许抖动*来消除死存储; 在实践中我相信它没有. (2认同)

Eri*_*ert 25

首先,Henk和Olivier的答案是正确的; 我想以稍微不同的方式解释它.具体来说,我想谈谈你提出的这一点.你有这套陈述:

int k = 10;
int c = 30;
k += c += k += c;
Run Code Online (Sandbox Code Playgroud)

然后你错误地得出结论,这应该给出与这组语句相同的结果:

int k = 10;
int c = 30;
k += c;
c += k;
k += c;
Run Code Online (Sandbox Code Playgroud)

了解你是如何做错的,以及如何正确地做到这一点是有益的.打破它的正确方法就是这样.

首先,重写最外面的+ =

k = k + (c += k += c);
Run Code Online (Sandbox Code Playgroud)

第二,重写最外面的+. 我希望您同意x = y + z必须始终与"将y评估为临时,将z评估为临时,将临时值相加,将总和分配给x"相同.所以让我们说清楚:

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;
Run Code Online (Sandbox Code Playgroud)

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

好了,现在再次,慢慢地,小心地将作业分解为t2.

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;
Run Code Online (Sandbox Code Playgroud)

赋值将为分配给c的t2分配相同的值,所以让我们说:

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;
Run Code Online (Sandbox Code Playgroud)

大.现在打破第二行:

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;
Run Code Online (Sandbox Code Playgroud)

太好了,我们正在取得进展.将作业细分为t4:

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;
Run Code Online (Sandbox Code Playgroud)

现在分解第三行:

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

所以当我们完成时,k是80而c是70.

现在让我们看一下IL在IL中的实现方式:

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3
Run Code Online (Sandbox Code Playgroud)

现在这有点棘手:

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
Run Code Online (Sandbox Code Playgroud)

我们可以将上面的内容实现为

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
Run Code Online (Sandbox Code Playgroud)

但我们使用"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.
Run Code Online (Sandbox Code Playgroud)

最后:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.
Run Code Online (Sandbox Code Playgroud)

由于我们不需要其他任何金额,我们不会重复它.堆栈现在是空的,我们在声明的最后.

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

  • @ OlivierJacot-Descombes:这是完全正确的.优先级和关联性与评估子表达式的顺序没有任何关系,除了优先级和关联性*确定子表达式边界的位置*之外.Subexpressions从左到右进行评估. (4认同)
  • @ OlivierJacot-Descombes:规范的相关行在"运算符"部分,并说"表达式中的操作数从左到右进行计算.例如,在`F(i)+ G(i ++)*H(i )`,**方法F使用旧的i值调用,然后使用旧值i调用方法G,最后,使用新值i**调用方法H.这与与运营商优先权无关." (强调补充说.)所以当我说没有"旧的价值被使用"的地方发生时,我想我错了!它出现在一个例子中.但*规范*位是"从左到右". (2认同)

Hen*_*man 14

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

答案是虽然作业从右到左绑定,但操作仍然从左到右进行.

所以最左边的+=是执行10 += 70.