Есть ли объяснение для встроенных операторов в "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 !!!