Есть ли объяснение для встроенных операторов в "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 говорит мне, что это значение не используется в других местах)?

Почему к 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??????

Ответы

Ответ 1

Операция типа 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 ниже и предоставленную ссылку Wai Ha Lee). Следовательно, вы не получаете 70 + 40 (новое значение k) = 110, но 70 + 10 (старое значение k) = 80.

Дело в том, что (согласно спецификации С#) "Операнды в выражении вычисляются слева направо" (операнды - это переменные c и k в нашем случае). Это не зависит от приоритета оператора и ассоциативности, которые в этом случае определяют порядок выполнения справа налево. (Смотрите комментарии к ответу Эрика Липперта на этой странице).


Теперь давайте посмотрим на 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: dup, IL_000d: stloc.0, т. IL_000d: stloc.0 Первое назначение для k, может быть оптимизировано. Вероятно, это делается для переменных джиттером при преобразовании IL в машинный код.

Также обратите внимание, что все значения, необходимые для расчета, либо помещаются в стек перед выполнением какого-либо назначения, либо рассчитываются по этим значениям. Присвоенные значения (по stloc) никогда не используются повторно во время этой оценки. stloc выскакивает из верхней части стека.


Результат следующего теста консоли: (Режим Release с включенными оптимизациями)

оценивая k (10)
оценка с (30)
оценивая k (10)
оценка с (30)
40 назначен к
70 назначено с
80 назначен к

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

Ответ 2

Во-первых, ответы Хенка и Оливье верны; Я хочу объяснить это немного по-другому. В частности, я хочу затронуть этот момент, который вы высказали. У вас есть этот набор утверждений:

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

Итак, когда мы закончим, к 80 и с 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", потому что он делает код короче и облегчает джиттер, и мы получаем тот же результат. В целом, генератор кода на С# старается поддерживать временные "эфемерные" значения в стеке в максимально возможной степени. Если вам легче следовать 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.

Поскольку нам не нужна сумма для чего-либо еще, мы не копируем ее. Стек теперь пуст, и мы в конце оператора.

Мораль этой истории такова: когда вы пытаетесь понять сложную программу, всегда разбивайте операции по одному. Не делайте коротких путей; они приведут вас в заблуждение.

Ответ 3

Это сводится к: применяется ли самый первый += к исходному k или к значению, которое было вычислено больше справа?

Ответ заключается в том, что хотя назначения связываются справа налево, операции по-прежнему продолжаются слева направо.

Таким образом, самый левый += выполняет 10 += 70.

Ответ 4

Я попробовал пример с gcc и pgcc и получил 110. Я проверил сгенерированный ими IR, и компилятор расширил expr до:

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

что выглядит разумным для меня.

Ответ 5

Вы можете решить это, считая.

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; k += c; c += k; k += c; и, следовательно, почему вы получаете разные ответы.

Некоторые люди в комментариях, похоже, обеспокоены тем, что вы можете попытаться обобщить из этого ярлыка все возможные типы дополнений. Итак, я поясню, что этот ярлык применим только к этой ситуации, то есть объединяет присваивания для встроенных типов чисел. Это (не обязательно) не работает, если вы добавляете другие операторы, например, () или +, или если вы вызываете функции, или если вы переопределили +=, или если вы используете что-то, кроме базовых типов чисел. Это только означало помочь с конкретной ситуацией в вопросе.

Ответ 6

для этого вида цепных присвоений вы должны присваивать значения, начиная с самой правой стороны. Вы должны назначить и рассчитать и назначить его левой стороне, и пройти весь этот путь до последнего (крайнее левое назначение), конечно, он рассчитывается как k = 80.

Ответ 7

Простой ответ: замените переменные значениями и получите:

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