C# “中是否有内联运算符的解释?”;k+;=c+;=k+;=c&引用;?

C# “中是否有内联运算符的解释?”;k+;=c+;=k+;=c&引用;?,c#,cil,compound-assignment,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不起作用(实际上VisualStudio告诉我该值没有在其他地方使用) 为什么K80不是110 如果我将操作拆分为: k+=c; c+=k; k+=c; 结果是k=110 我试着看完,但我对解释生

对以下操作的结果有何解释

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不起作用(实际上VisualStudio告诉我该值没有在其他地方使用)

为什么K80不是110

如果我将操作拆分为:

k+=c;
c+=k;
k+=c;
结果是k=110

我试着看完,但我对解释生成的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
的值尤其如此(请参阅我对下面的IL和提供的Wai Ha Lee的评论)。因此,您得到的不是70+40(新值
k
)=110,而是70+10(旧值
k
)=80

要点是(根据C#)“表达式中的操作数是从左到右求值的”(在我们的例子中,操作数是变量
C
k
)。这与运算符优先级和关联性无关,在本例中,运算符优先级和关联性指示从右到左的执行顺序。(参见本页对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)
堆栈现在看起来是这样的(从左到右;堆栈顶部是右侧)

10301030

103040

10304040

1070

107070

八十


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

答案是,虽然任务从右向左绑定,但操作仍然从左向右进行


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

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

int k = 10;
int c = 30;
k += c += k += c;
int k = 10;
int c = 30;
k += c;
c += k;
k += c;
然后,您错误地得出结论,这应该与这组语句得出相同的结果:

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.
因为我们不需要其他任何东西的总和,我们不重复它。堆栈现在是空的,我们在语句的末尾


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

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

a = k += c += k += c
有两个
c
s和两个
k
s所以

a = 2c + 2k
而且,作为语言运算符的结果,
k
也等于
2c+2k

这将适用于此类型链中的任何变量组合:

a = r += r += r += m += n += m
所以

r
将等于相同的值

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

这表明
k+=c+=k+=c
不同于
k+=c;c+=k;k+=c以及为什么会得到不同的答案

评论中的一些人似乎担心,您可能会试图将此快捷方式过度概括为所有可能的添加类型。因此,我要明确指出,此快捷方式仅适用于这种情况,即将内置数字类型的加法赋值链接在一起。如果在中添加其他运算符,例如
()
+
,或者调用函数,或者覆盖了
+=
,或者使用的不是基本的数字类型,则不一定有效它仅用于帮助解决问题中的特定情况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
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
a = 2c + 2k
a = r += r += r += m += n += m
a = 2m + n + 3r
int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!
k = 10;
c = 30;
k = c+k;
c = c+k;
k = c+k;