Чем объясняется результат следующей операции?
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 сообщает мне, что это значение больше нигде не используется)?
Почему 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??????
c#
cil
compound-assignment
Андрей Котляров
источник
источник
Ответы:
Операция вроде
a op= b;
эквивалентнаa = a op b;
. Присваивание может использоваться как оператор или как выражение, а как выражение оно дает присвоенное значение. Ваше заявление ...... может, поскольку оператор присваивания правоассоциативен, также может быть записан как
или (развернутый)
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 ниже и ссылку, предоставленную Вай Ха Ли). Следовательно, вы получаете не 70 + 40 (новое значениеk
) = 110, а 70 + 10 (старое значениеk
) = 80.Дело в том , что ( в соответствии с # C спецификации ) «Операнды в выражении вычисляется слева направо» (операнды являются переменными ,
c
иk
в нашем случае). Это не зависит от приоритета и ассоциативности операторов, которые в данном случае определяют порядок выполнения справа налево. (См. Комментарии к ответу Эрика Липперта на этой странице).Теперь посмотрим на Ил. 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)
Стек теперь выглядит так (слева направо; верх стека справа)
IL_000b: add // pops the 2 top (right) positions, adds them and pushes the sum back
IL_000d: stloc.0 // k <-- 40
IL_000e: add
IL_0010: stloc.1 // c <-- 70
IL_0011: add
IL_0012: stloc.0 // k <-- 80
Обратите внимание, что
IL_000c: dup
,IL_000d: stloc.0
т.е. первое присвоениеk
, можно оптимизировать. Вероятно, это делается для переменных по джиттеру при преобразовании IL в машинный код.Также обратите внимание, что все значения, требуемые для вычисления, либо помещаются в стек перед выполнением какого-либо назначения, либо вычисляются на основе этих значений. Присвоенные значения (by
stloc
) никогда не используются повторно во время этой оценки.stloc
выскакивает верх стека.Результат следующего консольного теста (
Release
режим с включенной оптимизацией)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; }
источник
k = 10 + (30 + (10 + 30)) = 80
и этоc
окончательное значение устанавливается в первой круглой скобкеc = 30 + (10 + 30) = 70
.k
это локальное хранилище, то мертвое хранилище почти наверняка будет удалено, если оптимизация включена, и сохранена, если это не так. Интересный вопрос: разрешено ли джиттеру исключать мертвое хранилище, еслиk
это поле, свойство, слот массива и так далее; на практике я считаю, что это не так.k
он назначается дважды, если это свойство.Во-первых, ответы Хенка и Оливье верны; Я хочу объяснить это немного иначе. В частности, я хочу коснуться вашего замечания. У вас есть такой набор утверждений:
int k = 10; int c = 30; k += c += k += c;
И затем вы ошибочно заключаете, что это должно дать тот же результат, что и этот набор утверждений:
int k = 10; int c = 30; k += c; c += k; 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.
Поскольку эта сумма нам ни на что не нужна, мы ее не дублируем. Стек теперь пуст, и мы подошли к концу оператора.
Мораль этой истории такова: когда вы пытаетесь понять сложную программу, всегда разбивайте операции по одной . Не выбирайте короткие пути; они сбивают вас с пути.
источник
F(i) + G(i++) * H(i)
, Методе F называется используя старое значение I, то метод G вызывается со старым значением i, и, наконец, метод H вызывается с новым значением i . Это отделено от приоритета оператора и не связано с ним ». (Курсив добавлен.) Думаю, я был неправ, когда сказал, что нигде не встречается выражение «используется старое значение»! Это происходит в примере. Но нормативный бит - «слева направо».+
, и тогда вы получите+=
бесплатно, потому чтоx += y
определяется какx = x + y
exceptx
, оценивается только один раз. Это верно независимо от того,+
является ли он встроенным или определяемым пользователем. Итак: попробуйте перегрузить+
ссылочный тип и посмотрите, что произойдет.Это сводится к следующему: самое первое
+=
применяется к оригиналуk
или к значению, которое было вычислено правее?Ответ заключается в том, что, хотя присваивания связываются справа налево, операции по-прежнему выполняются слева направо.
Таким образом, крайний левый
+=
выполняют10 += 70
.источник
Я попробовал пример с gcc и pgcc и получил 110. Я проверил сгенерированный ими IR, и компилятор расширил выражение до:
k = 10; c = 30; k = c+k; c = c+k; k = c+k;
что мне кажется разумным.
источник
для такого типа цепных назначений вам нужно назначать значения, начиная с самой правой стороны. Вы должны назначить и вычислить и назначить его левой стороне, и проделать весь путь до последнего (крайнего левого назначения). Конечно, он рассчитывается как k = 80.
источник
Простой ответ: замените vars значениями, и вы его получили:
int k = 10; int c = 30; k += c += k += c; 10 += 30 += 10 += 30 = 10 + 30 + 10 + 30 = 80 !!!
источник
k = 10; m = (k += k) + k;
не значитm = (10 + 10) + 10
. Языки с изменяющимися выражениями нельзя анализировать так, как если бы они имели активную замену значений . Замена значений происходит в определенном порядке по отношению к мутациям, и вы должны это учитывать.Вы можете решить эту проблему, посчитав.
Есть два
c
и два,k
так чтоa = 2c + 2k
И, как следствие операторов языка,
k
также равно2c + 2k
Это будет работать для любой комбинации переменных в этом стиле цепочки:
Так
a = 2m + n + 3r
И
r
будет равняться так же.Вы можете вычислить значения других чисел, вычисляя только до их крайнего левого назначения. Так что
m
равные2m + n
иn
равныеn + m
.Это демонстрирует, что
k += c += k += c;
отличается отk += c; c += k; k += c;
и, следовательно, почему вы получаете разные ответы.Некоторые люди в комментариях, кажется, обеспокоены тем, что вы можете попытаться обобщить этот ярлык на все возможные типы дополнений. Итак, я поясню, что этот ярлык применим только к этой ситуации, то есть для объединения в цепочку дополнительных назначений для встроенных числовых типов. Это не (обязательно) работает, если вы добавляете другие операторы, например,
()
или+
, или если вы вызываете функции, или если вы переопределяете+=
, или если вы используете что-то другое, кроме основных типов чисел. Это предназначено только для того, чтобы помочь в конкретной ситуации в вопросе .источник
x = 1;
иy = (x += x) + x;
утверждаете ли вы, что «есть три x, и поэтому y равно3 * x
»? Потомуy
что4
в этом случае равно . А как насчет тогоy = x + (x += x);
, что вы утверждаете, что выполняется алгебраический закон «a + b = b + a», и это тоже 4? Потому что это 3. К сожалению, C # не следует правилам школьной алгебры, если в выражениях есть побочные эффекты . C # следует правилам алгебры побочных эффектов.