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

为什么是 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??????
6352 次浏览

An operation like a op= b; is equivalent to a = a op b;. An assignment can be used as statement or as expression, while as expression it yields the assigned value. Your statement ...

k += c += k += c;

... can, since the assignment operator is right-associative, also be written as

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

or (expanded)

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

Where during the whole evaluation the old values of the involved variables are used. This is especially true for the value of k (see my review of the IL below and the link Wai Ha Lee provided). Therefore, you are not getting 70 + 40 (new value of k) = 110, but 70 + 10 (old value of k) = 80.

The point is that (according to the 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 answer on this page).


Now let's look at the IL. IL assumes a stack based virtual machine, i.e. it does not use registers.

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)

The stack now looks like this (from left to right; top of stack is right)

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

Note that IL_000c: dup, IL_000d: stloc.0, i.e. the first assignment to k , could be optimized away. Probably this is done for variables by the jitter when converting IL to machine code.

Note also that all the values required by the calculation are either pushed to the stack before any assignment is made or are calculated from these values. Assigned values (by stloc) are never re-used during this evaluation. stloc pops the top of the stack.


The output of the following console test is (Release mode with optimizations on)

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

It boils down to: is the very first += applied to the original k or to the value that was computed more to the right ?

The answer is that although assignments bind from right to left, operations still proceed from left to right.

So the leftmost += is executing 10 += 70.

First off, Henk and Olivier's answers are correct; I want to explain it in a slightly different way. Specifically, I want to address this point you made. You have this set of statements:

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

And you then incorrectly conclude that this should give the same result as this set of statements:

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

It is informative to see how you got that wrong, and how to do it right. The right way to break it down is like this.

First, rewrite the outermost +=

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

Second, rewrite the outermost +. I hope you agree that x = y + z must always be the same as "evaluate y to a temporary, evaluate z to a temporary, sum the temporaries, assign the sum to x". So let's make that very explicit:

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

Make sure that is clear, because this is the step you got wrong. When breaking down complex operations into simpler operation you must make sure that you do so slowly and carefully and do not skip steps. Skipping steps is where we make mistakes.

OK, now break down the assignment to t2, again, slowly and carefully.

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

The assignment will assign the same value to t2 as is assigned to c, so let's say that:

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

Great. Now break down the second line:

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

Great, we are making progress. Break down the assignment to t4:

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

Now break down the third line:

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

And now we can look at the whole thing:

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

So when we are done, k is 80 and c is 70.

Now let's look at how this is implemented in the IL:

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

Now this is a bit tricky:

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

We could have implemented the above as

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

but we use the "dup" trick because it makes the code shorter and makes it easier on the jitter, and we get the same result. In general, the C# code generator tries to keep temporaries "ephemeral" on the stack as much as possible. If you find it easier to follow the IL with fewer ephemerals, turn optimizations off, and the code generator will be less aggressive.

We now have to do the same trick to get 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.

and finally:

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

Since we do not need the sum for anything else, we do not dup it. The stack is now empty, and we're at the end of the statement.

The moral of the story is: when you are trying to understand a complicated program, always break down operations one at a time. Don't take short cuts; they will lead you astray.

You can solve this by counting.

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

There are two cs and two ks so

a = 2c + 2k

And, as a consequence of the operators of the language, k also equals 2c + 2k

This will work for any combination of variables in this style of chain:

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

So

a = 2m + n + 3r

And r will equal the same.

You can work out the values of the other numbers by only calculating up to their leftmost assignment. So m equals 2m + n and n equals n + m.

This demonstrates that k += c += k += c; is different to k += c; c += k; k += c; and hence why you get different answers.

Some folks in the comments seem to be worried that you might try to over generalise from this shortcut to all possible types of addition. So, I'll make it clear that this shortcut is only applicable to this situation, i.e. chaining together addition assignments for the built in number types. It doesn't (necessarily) work if you add other operators in, e.g. () or +, or if you call functions or if you've overriden +=, or if you're using something other than the basic number types. It's only meant to help with the particular situation in the question.

for this kind of chain assignments, you have to assign the values from starting at the most right side. You have to assign and calculate and assign it to left side, and go on this all the way to the final (leftmost assignment), Sure it is calculated as k=80.

Simple answer: Replace vars with values und you got it:

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

I tried the example with gcc and pgcc and got 110. I checked the IR they generated, and compiler did expand the expr to:

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

which looks reasonable to me.