Пост-приращение в рамках самоопределения

Я понимаю различия между i++ and ++i, но я не совсем уверен, почему я получаю результаты ниже:

static void Main(string[] args)
{
    int c = 42;
    c = c++;
    Console.WriteLine(c);   //Output: 42
}

В приведенном выше коде, поскольку это назначает переменную самому себе и затем увеличивает значение, я ожидаю, что результат будет 43. Однако он возвращает 42. Я получаю тот же результат при использовании c = c--;.

Я понимаю, что могу просто использовать c++; и делать с ним, но мне более любопытно, почему он ведет себя так, как есть. Может ли кто-нибудь объяснить, что здесь происходит?

Ответ 1

Давайте рассмотрим код языка посредника для этого:

IL_0000:  nop
IL_0001:  ldc.i4.s    2A
IL_0003:  stloc.0     // c
IL_0004:  ldloc.0     // c

Это загружает константное целое число 42 в стек, а затем сохраняет его в переменной c и сразу же загружает в стек.

IL_0005:  stloc.1
IL_0006:  ldloc.1

Это копирует значение в другой регистр и снова загружает его.

IL_0007:  ldc.i4.1
IL_0008:  add

Это добавляет константу 1 к загруженному значению

IL_0009:  stloc.0     // c

... и сохраняет результат (43) в переменной c.

IL_000A:  ldloc.1
IL_000B:  stloc.0     // c

Затем загружается значение из другого регистра (это все еще 42!) и сохраняется в переменной c.

IL_000C:  ldloc.0     // c
IL_000D:  call        System.Console.WriteLine
IL_0012:  nop
IL_0013:  ret

Затем значение (42) загружается из переменной и печатается.


Итак, что вы можете видеть из этого, так это то, что while c++ увеличивает значение переменной на единицу после того, как результат был возвращен, это увеличение происходит еще до того, как будет задано значение переменной. Таким образом, последовательность выглядит примерно так:

  • Получить значение из c
  • Пост-приращение c
  • Назначить ранее прочитанное значение c

И это должно объяснить, почему вы получаете этот результат:)


Чтобы добавить еще один пример, поскольку это было упомянуто в комментарии, который был удален:

c = c++ + c;

Это работает очень похоже: если сначала ввести начальное значение 2, сначала оценивается левая часть сложения. Таким образом, значение считывается из переменной (2), затем c увеличивается (c становится 3). Затем оценивается правая часть сложения. Значение c читается (теперь 3). Тогда добавление имеет место (2 + 3), а результат (5) присваивается переменной.


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

Ответ 2

Согласно странице MSDN на операторах С# оператор присваивания (=) имеет более низкий приоритет, чем любой первичный оператор, например ++x или x++. Это означает, что в строке

c = c++;

сначала оценивается правая часть. Выражение c++ увеличивает c до 43, а затем возвращает исходное значение 42 в результате, которое используется для назначения.

Как документация, связанная с состояниями,

[Вторая форма - это операция приращения постфикса. Результатом операции является значение операнда до того, как он будет увеличен.

Другими словами, ваш код эквивалентен

// Evaluate the right hand side:
int incrementResult = c;   // Store the original value, int incrementResult = 42
c = c + 1;                 // Increment c, i.e. c = 43

// Perform the assignment:
c = incrementResult;       // Assign c to the "result of the operation", i.e. c = 42

Сравните это с формой префикса

c = ++c;

который будет оцениваться как

// Evaluate the right hand side:
c = c + 1;                 // Increment c, i.e. c = 43
int incrementResult = c;   // Store the new value, i.e. int incrementResult = 43

// Perform the assignment:
c = incrementResult;       // Assign c to the "result of the operation", i.e. c = 43

Ответ 3

Документы говорят о состоянии постфикса:

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

Это означает, что когда вы выполните:

c = c++;

На самом деле вы переназначаете 42 на c, и почему вы видите консольную печать 42. Но если вы это сделаете:

static void Main(string[] args)
{
    int c = 42;
    c++;
    Console.WriteLine(c);  
}

Вы увидите вывод 43.

Если вы посмотрите, что генерирует компилятор (в режиме отладки), вы увидите:

private static void Main(string[] args)
{
    int num = 42;
    int num2 = num;
    num = num2 + 1;
    num = num2;
    Console.WriteLine(num);
}

Что более ясно отображает перезапись. Если вы посмотрите в режиме Release, вы увидите, что компилятор оптимизирует весь вызов:

private static void Main(string[] args)
{
    Console.WriteLine(42);
}

Ответ 4

... поскольку это назначает переменную самому себе, а затем увеличивает значение...

Нет, это не то, что он делает.

Оператор post -increment увеличивает эту переменную, а возвращает старое значение. Оператор pre -increment увеличивает эту переменную, а возвращает новое значение.

Итак, ваш c++ увеличивает c до 43, но возвращает 42, который затем снова присваивается c.

Ответ 5

Выражение в правой части присваивания оценивается полностью, затем выполняется присвоение.

   c = c++;

То же, что и

   // Right hand side is calculated first.
   _tmp = c;
   c = c + 1;

   // Then the assignment is performed
   c = _tmp;

Ответ 6

Думаю, я понимаю, о чем думал первоначальный вопросник. Они думали (я думаю), что postincrement означает приращение переменной после оценки всего выражения, например. что

x = a[i++] + a[j++];   // (0)

совпадает с

{ x = a[i] + a[j] ; i += 1 ; j += 1; }   // (1)

(и, по общему признанию, они эквивалентны) и что

c = c++;  // (2)

означает

{ c = c ; c +=1 ; } // (3)

и что

x = a[i++] + a[i++];  // (4)

означает

{ x = a[i] + a[i] ; i += 2 ; } // (5)

Но это не так. v++ означает приращение v сразу, но используйте старое значение как значение выражения. Итак, в случае (4) фактически эквивалентная инструкция

{int t0 = a[i] ; i += 1 ; int t1 = a[i] ; i += 1 ; x = t0 + t1 ; } // (6)

Как отмечали другие, утверждения, подобные (2) и (4), хорошо определены в С# (и Java), но они не определены корректно в C и С++.

В выражениях C и С++, таких как (2) и (4), которые изменяют переменную, а также используют ее каким-либо другим способом, обычно undefined, что означает, что компилятор приветствуется (в отношении языковых законов) для перевода их вообще, например, для перевода денег с вашего банковского счета на автора компилятора.