Есть ли объяснение встроенным операторам в «k + = c + = k + = c;»?


89

Чем объясняется результат следующей операции?

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??????

3
У вас другой результат, потому что вы разделили функцию, k + = c + = k + = c = 80, потому что значения k и c остаются одинаковыми во всех суммах, поэтому k + = c + = k + = c равно
João Paulo Amorim

78
Интересное упражнение, но на практике никогда не пишите подобную цепочку кода, если только вы не хотите, чтобы коллеги вас ненавидели. :)
UnhandledExcepSean

3
@AndriiKotliarov, потому что k + = c + = k + = c равно 10 + 30 + 10 + 30, поэтому K получает все значения, а C получает только последние 3 аргумента 30 + 10 + 30 = 70
João Paulo Amorim

6
Также стоит читать - Эрик Липперта ответ на В чем разница между I ++ и ++ я?
Вай Ха Ли

34
«Доктор, доктор, мне больно, когда я это делаю!» «Так что не делай этого».
Дэвид Конрад,

Ответы:


104

Операция вроде a op= b;эквивалентна a = a op b;. Присваивание может использоваться как оператор или как выражение, а как выражение оно дает присвоенное значение. Ваше заявление ...

k += c += k += c;

... может, поскольку оператор присваивания правоассоциативен, также может быть записан как

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

или (развернутый)

k =  k +  (c = c +  (k = k  + c));
     10301030   // operand evaluation order is from left to right
      |         |        ↓    ↓
      |         ↓   4010 + 30   // operator evaluation7030 + 40
8010 + 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)

Стек теперь выглядит так (слева направо; верх стека справа)

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

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


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

оценка k (10)
оценка c (30)
оценка k (10)
оценка c (30)
40 присвоено k
70 присвоено c
80 присвоено 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;
}

Вы можете добавить окончательный результат с числами в формуле для еще большей полноты: final is k = 10 + (30 + (10 + 30)) = 80и это cокончательное значение устанавливается в первой круглой скобке c = 30 + (10 + 30) = 70.
Franck

2
В самом деле, если kэто локальное хранилище, то мертвое хранилище почти наверняка будет удалено, если оптимизация включена, и сохранена, если это не так. Интересный вопрос: разрешено ли джиттеру исключать мертвое хранилище, если kэто поле, свойство, слот массива и так далее; на практике я считаю, что это не так.
Эрик Липперт,

Консольный тест в режиме Release действительно показывает, что kон назначается дважды, если это свойство.
Оливье Жако-Декомб,

26

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

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.

Поскольку эта сумма нам ни на что не нужна, мы ее не дублируем. Стек теперь пуст, и мы подошли к концу оператора.

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


3
@ OlivierJacot-Декомбы: Соответствующая строка спецификации в разделе «Операторы» и говорят «Операнды в выражении вычисляются слева направо , например, в. F(i) + G(i++) * H(i), Методе F называется используя старое значение I, то метод G вызывается со старым значением i, и, наконец, метод H вызывается с новым значением i . Это отделено от приоритета оператора и не связано с ним ». (Курсив добавлен.) Думаю, я был неправ, когда сказал, что нигде не встречается выражение «используется старое значение»! Это происходит в примере. Но нормативный бит - «слева направо».
Эрик Липперт,

1
Это было недостающее звено. Квинтэссенция состоит в том, что мы должны различать порядок оценки операндов и приоритет операторов . Оценка операнда идет слева направо, а в случае OP выполнение оператора справа налево.
Olivier Jacot-Descombes

4
@ OlivierJacot-Descombes: Совершенно верно. Приоритет и ассоциативность не имеют никакого отношения к порядку, в котором оцениваются подвыражения, кроме того факта, что приоритет и ассоциативность определяют, где находятся границы подвыражений . Подвыражения оцениваются слева направо.
Эрик Липперт,

1
Ой, похоже, вы не можете перегрузить операторы присваивания: /
johnny 5

1
@ johnny5: Верно. Но вы можете перегрузить +, и тогда вы получите +=бесплатно, потому что x += yопределяется как x = x + yexcept x, оценивается только один раз. Это верно независимо от того, +является ли он встроенным или определяемым пользователем. Итак: попробуйте перегрузить +ссылочный тип и посмотрите, что произойдет.
Эрик Липперт

14

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

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

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


1
Это прекрасно помещает его в ореховую скорлупу.
Аганджу,

На самом деле это операнды, которые оцениваются слева направо.
Оливье Жако-Декомб

0

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

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

что мне кажется разумным.


-1

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


Пожалуйста, не публикуйте ответы, которые просто повторяют то, что уже сказано во многих других ответах.
Эрик Липперт

-1

Простой ответ: замените 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. Языки с изменяющимися выражениями нельзя анализировать так, как если бы они имели активную замену значений . Замена значений происходит в определенном порядке по отношению к мутациям, и вы должны это учитывать.
Эрик Липперт

-1

Вы можете решить эту проблему, посчитав.

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

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


Это не ответ на вопрос
Джонни 5

@ johnny5 объясняет, почему вы получаете результат, который получаете, то есть потому, что так работает математика.
Мэтт Эллен

2
Математика и порядок операций, которые компилятор оценивает для оператора, - это две разные вещи. По вашей логике k + = c; c + = k; k + = c должно давать тот же результат.
Джонни 5

Нет, Джонни 5, это не то, что это значит. Математически это разные вещи. Три отдельные операции дают результат 3c + 2k.
Мэтт Эллен

2
К сожалению, ваше "алгебраическое" решение верно только по совпадению . Ваша техника вообще не работает . Подумайте x = 1;и y = (x += x) + x;утверждаете ли вы, что «есть три x, и поэтому y равно 3 * x»? Потому yчто 4в этом случае равно . А как насчет того y = x + (x += x);, что вы утверждаете, что выполняется алгебраический закон «a + b = b + a», и это тоже 4? Потому что это 3. К сожалению, C # не следует правилам школьной алгебры, если в выражениях есть побочные эффекты . C # следует правилам алгебры побочных эффектов.
Эрик Липперт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.