В чем разница между i ++ и ++ i?

204

Я видел их обоих используются в многочисленных кусков кода С #, и я хотел бы знать , когда использовать i++или ++i( iбудучи переменным числом , как int, float, doubleи т.д.). Кто-нибудь, кто знает это?

Dlaor
источник
13
За исключением случаев, когда это не имеет значения, вы никогда не должны использовать ни один из них, потому что это просто заставит людей задавать тот же вопрос, который вы задаете сейчас. «Не заставляй меня думать» относится как к коду, так и к дизайну.
Охотник за экземплярами
2
@Dlaor: Вы вообще читали ссылки в моем комментарии? Первый из них о C #, а второй - не зависит от языка, с принятым ответом, ориентированным на C #.
gnovice
7
@gnovice, первый спрашивает о разнице в производительности, а я спрашивал о фактической разнице в коде, второй - о разнице в цикле, а я спрашивал о разнице в целом, а третий - о C ++.
Dlaor
1
Я считаю, что это не дубликат разницы между i ++ и ++ i в цикле? - как говорит Длоар в своем комментарии выше, другой вопрос конкретно касается использования внутри цикла.
чи х

Ответы:

201

Как ни странно, остальные два ответа ничего не объясняют, и определенно стоит сказать:


i++означает «скажи мне значение i, а затем увеличить»

++iозначает «приращение i, а затем скажите мне значение»


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

Кирен Джонстон
источник
11
Кажется, это противоречит тому, что говорит Эрик
Эван Кэрролл
65
Ни одно из утверждений не является правильным. Рассмотрим ваше первое утверждение. На самом деле i ++ означает «сохранить значение, увеличить его, сохранить в i, а затем сообщить мне исходное сохраненное значение». То есть, сообщение происходит после увеличения, а не до того, как вы это указали. Рассмотрим второе утверждение. На самом деле i ++ означает «сохранить значение, увеличить его, сохранить в i и сообщить мне увеличенное значение». То, как вы сказали, делает неясным, является ли значение значением i или значением, которое было присвоено i; они могут быть разными .
Эрик Липперт
8
@ Эрик, мне кажется даже с твоим ответом, второе утверждение категорически верно. Хотя он исключает несколько шагов.
Эван Кэрролл
20
Конечно, в большинстве случаев неаккуратные, неправильные способы описания операционной семантики дают те же результаты, что и точное и правильное описание. Во-первых, я не вижу убедительной ценности в получении правильных ответов с помощью неправильных рассуждений, а во-вторых, да, я видел производственный код, который делает именно такие вещи неправильно. Я, вероятно, получаю полдюжины вопросов от настоящих программистов в год о том, почему какое-то конкретное выражение, переполненное инкрементами, декрементами и разыменованиями массивов, не работает так, как они предполагали.
Эрик Липперт
12
@supercat: речь идет о C #, а не о C.
Танатос
428

Типичный ответ на этот вопрос, к сожалению, уже опубликованный здесь, заключается в том, что один выполняет приращение «до» оставшихся операций, а другой - «после» оставшихся операций. Хотя это интуитивно передает идею, это утверждение на первый взгляд совершенно неверно . Последовательность событий во время очень хорошо определена в C #, и это решительно не тот случай, когда префикс (++ вара) и постфикс (вар ++) версия ++ делать вещи в другом порядке по отношению к другим операциям.

Неудивительно, что вы увидите много неправильных ответов на этот вопрос. Огромное количество книг "научи себя C #" также ошибаются. Кроме того, способ, которым C # делает это, отличается от того, как это делает C. Многие люди считают, что C # и C - это один и тот же язык; они не. Конструкция операторов приращения и убывания в C #, на мой взгляд, позволяет избежать конструктивных недостатков этих операторов в C.

Есть два вопроса, на которые нужно ответить, чтобы определить, что именно происходит с префиксом и постфиксом ++ в C #. Первый вопрос: каков результат? и второй вопрос: когда происходит побочный эффект приращения?

Не ясно, каков ответ на любой вопрос, но на самом деле все довольно просто, когда вы его видите. Позвольте мне объяснить вам, что именно x ++ и ++ x делают для переменной x.

Для префиксной формы (++ x):

  1. х оценивается для получения переменной
  2. Значение переменной копируется во временную папку
  3. Временное значение увеличивается для получения нового значения (не перезаписывая временное!)
  4. Новое значение сохраняется в переменной
  5. Результатом операции является новое значение (то есть увеличенное значение временного)

Для формы постфикса (x ++):

  1. х оценивается для получения переменной
  2. Значение переменной копируется во временную папку
  3. Временное значение увеличивается для получения нового значения (не перезаписывая временное!)
  4. Новое значение сохраняется в переменной
  5. Результатом операции является значение временного

Некоторые вещи, на которые стоит обратить внимание:

Во-первых, порядок событий во времени одинаков в обоих случаях . Опять же, это абсолютно не тот случай, когда порядок событий во времени изменяется между префиксом и постфиксом. Совершенно неверно говорить, что оценка происходит до других оценок или после других оценок. Оценки выполняются в одном и том же порядке в обоих случаях, как можно видеть на шагах с 1 по 4, которые идентичны. Лишь отличием является последним шагом - будь то результатом является значение временного или нового, увеличивающегося значения.

Вы можете легко продемонстрировать это с помощью простого консольного приложения C #:

public class Application
{
    public static int currentValue = 0;

    public static void Main()
    {
        Console.WriteLine("Test 1: ++x");
        (++currentValue).TestMethod();

        Console.WriteLine("\nTest 2: x++");
        (currentValue++).TestMethod();

        Console.WriteLine("\nTest 3: ++x");
        (++currentValue).TestMethod();

        Console.ReadKey();
    }
}

public static class ExtensionMethods 
{
    public static void TestMethod(this int passedInValue) 
    {
        Console.WriteLine("Current:{0} Passed-in:{1}",
            Application.currentValue,
            passedInValue);
    }
}

Вот результаты ...

Test 1: ++x
Current:1 Passed-in:1

Test 2: x++
Current:2 Passed-in:1

Test 3: ++x
Current:3 Passed-in:3

В первом тесте вы можете увидеть, что currentValueи то, что было переданоTestMethod() расширению, показывают одинаковое значение, как и ожидалось.

Однако во втором случае люди попытаются сообщить вам, что приращение currentValueпроисходит после вызова TestMethod(), но, как вы можете видеть из результатов, это происходит до вызова, как указано в результате «Текущий: 2».

В этом случае сначала значение currentValueсохраняется во временном. Затем инкрементная версия этого значения сохраняется обратно, currentValueно без касания временного, в котором все еще сохраняется исходное значение. Наконец то временное передается TestMethod(). Если инкремент произошел после вызова, TestMethod()он дважды вывел бы одно и то же, не приращенное значение, но это не так.

Важно отметить, что значение, возвращаемое как из операций, так currentValue++и из ++currentValueопераций, основано на временном, а не фактическом значении, сохраненном в переменной в момент выхода из любой операции.

Напомним, что в порядке операций выше, первые два шага копируют текущее значение переменной во временное. Это то, что используется для расчета возвращаемого значения; в случае версии с префиксом это временное значение увеличивается, а в случае версии с суффиксом это значение напрямую / не увеличивается. Сама переменная не читается снова после первоначального хранения во временную.

Проще говоря, постфиксная версия возвращает значение, которое было прочитано из переменной (т. Е. Значение временного), в то время как префиксная версия возвращает значение, которое было записано обратно в переменную (т. Е. Увеличенное значение временного). Ни один из них не возвращает значение переменной.

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

Люди на удивление часто запутываются в отношении приоритета, ассоциативности и порядка выполнения побочных эффектов, я подозреваю, что в основном потому, что это так запутанно в C. C # был тщательно спроектирован, чтобы быть менее запутанным во всех этих отношениях. Для некоторого дополнительного анализа этих проблем, включая мою дальнейшую демонстрацию ложности идеи о том, что префиксные и постфиксные операции «перемещают вещи во времени», смотрите:

https://ericlippert.com/2009/08/10/precedence-vs-order-redux/

что привело к такому вопросу:

int [] arr = {0}; int value = arr [arr [0] ++]; Значение = 1?

Вас также могут заинтересовать мои предыдущие статьи на эту тему:

https://ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order/

и

https://ericlippert.com/2007/08/14/c-and-the-pit-of-despair/

и интересный случай, когда C затрудняет рассуждения о правильности:

https://docs.microsoft.com/archive/blogs/ericlippert/bad-recursion-revisited

Кроме того, мы сталкиваемся с похожими тонкими проблемами при рассмотрении других операций, которые имеют побочные эффекты, такие как цепочка простых назначений:

https://docs.microsoft.com/archive/blogs/ericlippert/chaining-simple-assignments-is-not-so-simple

И вот интересный пост о том, почему операторы приращения приводят к значениям в C #, а не в переменных :

Почему я не могу сделать ++ i ++ на C-подобных языках?

Эрик Липперт
источник
4
+1: Для действительно любопытных, не могли бы вы дать ссылку на то, что вы подразумеваете под «недостатками дизайна этих операций в C»?
Джастин Ардини
21
@Justin: я добавил несколько ссылок. Но в основном: в C у вас нет никаких гарантий относительно того, что происходит во времени. Соответствующий компилятор может делать все, что угодно, когда есть две мутации в одной и той же точке последовательности, и вам не нужно сообщать вам, что вы делаете что-то, определяемое реализацией. Это заставляет людей писать опасно непереносимый код, который работает на некоторых компиляторах и делает что-то совершенно другое для других.
Эрик Липперт
14
Я должен сказать, что для действительно любопытных это хорошее знание, но для среднего приложения на C # разница между формулировками в других ответах и ​​действительными вещами настолько далеко ниже уровня абстракции языка, что он действительно делает нет разницы. C # не ассемблер, и из 99,9% случаев i++или ++iиспользуется в коде, вещи, происходящие в фоновом режиме, просто так; на заднем плане . Я пишу на C #, чтобы подняться на уровни абстракции выше того, что происходит на этом уровне, поэтому, если это действительно имеет значение для вашего кода C #, возможно, вы уже не на том языке.
Томас Ашан
24
@ Томас: Во-первых, я обеспокоен всеми приложениями на C #, а не просто средними приложениями. Количество не средних приложений велико. Во-вторых, это не имеет значения, за исключением случаев, когда это имеет значение . Я получаю вопросы об этом, основываясь на реальных ошибках в реальном коде, вероятно, полдюжины раз в год. В-третьих, я согласен с вашей большей точкой зрения; вся идея ++ - это очень низкоуровневая идея, которая выглядит очень странно в современном коде. Это действительно только потому, что для C-подобных языков идиоматично иметь такого оператора.
Эрик Липперт
11
+1 но я должен сказать правду ... Это годы и годы, когда я использую только постфиксные операторы, которые не одиноки в ряду (так что это не так i++;) for (int i = 0; i < x; i++)... И я очень очень рад этому! (и я никогда не использую префиксный оператор). Если мне нужно написать что-то, что понадобится старшему программисту за 2 минуты, чтобы расшифровать ... Ну ... Лучше написать еще одну строку кода или ввести временную переменную :-) Я думаю, что ваша "статья" (я выиграл не называйте это «ответ») подтверждает мой выбор :-)
xanatos
23

Если у вас есть:

int i = 10;
int x = ++i;

тогда xбудет 11.

Но если у вас есть:

int i = 10;
int x = i++;

тогда xбудет 10.

Обратите внимание, как указывает Эрик, приращение происходит одновременно в обоих случаях, но это то, какое значение дается как результат, который отличается (спасибо, Эрик!).

Обычно я люблю использовать, ++iесли нет веских причин не делать этого. Например, при написании цикла мне нравится использовать:

for (int i = 0; i < 10; ++i) {
}

Или, если мне просто нужно увеличить переменную, я хотел бы использовать:

++x;

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

DCP
источник
2
Возможно, вы могли бы прояснить свои примеры, расположив строки кода по порядку, то есть, скажем, «var x = 10; var y = ++ x;» и "var x = 10; var y = x ++;" вместо.
Томас Ашан
21
Этот ответ опасно неправильный. Когда происходит приращение, оно не изменяется по отношению к оставшимся операциям, поэтому то, что в одном случае это делается перед оставшимися операциями, а в другом - после оставшихся операций, вводит в заблуждение. Приращение выполняется одновременно в обоих случаях . Разница заключается в том, какое значение дается в результате , а не при увеличении .
Эрик Липперт
3
Я отредактировал это, чтобы использовать iдля имени переменной, а не varкак ключевое слово C #.
Джефф Йейтс
2
Так что без назначения переменных и просто с указанием "i ++;" или "++ i;" даст точно такие же результаты или я ошибаюсь?
Dlaor
1
@Eric: Не могли бы вы уточнить, почему оригинальная формулировка "опасна"? Это приводит к правильному использованию, даже если детали неверны.
Джастин Ардини
7
int i = 0;
Console.WriteLine(i++); // Prints 0. Then value of "i" becomes 1.
Console.WriteLine(--i); // Value of "i" becomes 0. Then prints 0.

Отвечает ли это на ваш вопрос ?

потерянный
источник
4
Можно предположить, что приращение постфикса и уменьшение префикса не влияют на переменную: P
Андреас
5

Оператор работает так, что он увеличивается в одно и то же время, но если он находится перед переменной, выражение будет оцениваться с помощью увеличенной / уменьшенной переменной:

int x = 0;   //x is 0
int y = ++x; //x is 1 and y is 1

Если это после переменной, текущий оператор будет выполнен с исходной переменной, как если бы он еще не был увеличен / уменьшен:

int x = 0;   //x is 0
int y = x++; //'y = x' is evaluated with x=0, but x is still incremented. So, x is 1, but y is 0

Я согласен с dcp в использовании предварительного увеличения / уменьшения (++ x) без необходимости. Действительно, единственный раз, когда я использую постинкремент / декремент, это циклы while или циклы такого рода. Эти петли одинаковы:

while (x < 5)  //evaluates conditional statement
{
    //some code
    ++x;       //increments x
}

или

while (x++ < 5) //evaluates conditional statement with x value before increment, and x is incremented
{
    //some code
}

Вы также можете сделать это при индексации массивов и тому подобного:

int i = 0;
int[] MyArray = new int[2];
MyArray[i++] = 1234; //sets array at index 0 to '1234' and i is incremented
MyArray[i] = 5678;   //sets array at index 1 to '5678'
int temp = MyArray[--i]; //temp is 1234 (becasue of pre-decrement);

И т. Д.

Майк Уэбб
источник
4

Просто для записи, в C ++, если вы можете использовать любой (то есть), вы не заботитесь о порядке операций (вы просто хотите увеличить или уменьшить и использовать его позже), оператор префикса более эффективен, так как он не необходимо создать временную копию объекта. К сожалению, большинство людей используют posfix (var ++) вместо префикса (++ var), просто потому, что это то, что мы узнали изначально. (Меня об этом спросили в интервью). Не уверен, что это правда в C #, но я предполагаю, что это будет.

DevinEllingson
источник
2
Вот почему я использую ++ i в c #, когда используется один (т.е. не в большем выражении). Всякий раз, когда я вижу цикл ac # с i ++, я немного плачу, но теперь, когда я знаю, что в c # это фактически тот же самый код, я чувствую себя лучше. Лично я все еще буду использовать ++ i, потому что привычки умирают тяжело.
Микле