Есть ли объяснение для встроенных операторов в «k + = c + = k + = c;»? - PullRequest
0 голосов
/ 13 февраля 2019

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

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

Ответы [ 7 ]

0 голосов
/ 16 февраля 2019

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

int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!
0 голосов
/ 19 февраля 2019

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

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

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

0 голосов
/ 15 февраля 2019

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

0 голосов
/ 14 февраля 2019

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

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

Существует два c s и два k s, поэтому

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

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

0 голосов
/ 13 февраля 2019

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

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.

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

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

0 голосов
/ 13 февраля 2019

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

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

Итак, самый левый += выполняет 10 += 70.

0 голосов
/ 13 февраля 2019

Операция, подобная 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 # spec ) "Операнды в выражении вычисляются слева направо" (операнды - это переменные 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,то есть первое присвоение k, может быть оптимизировано.Вероятно, это делается для переменных джиттером при преобразовании IL в машинный код.

Обратите также внимание, что все значения, необходимые для вычисления, либо помещаются в стек перед выполнением какого-либо присвоения, либо рассчитываются по этим значениям.Присвоенные значения (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;
}
...