Яка різниця між i ++ та ++ i?


204

Я бачив їх обох використовуються в численних шматків коду С #, і я хотів би знати , коли використовувати i++або ++i( iбудучи змінним числом , як int, float, doubleі т.д.). Хто це знає?


13
За винятком випадків, коли це не має ніякої різниці, ніколи не слід використовувати жодне з них, адже це просто змусить людей задавати той самий питання, який ви задаєте зараз. "Не змушуй мене думати" стосується коду, а також дизайну.
Мисливець на екземпляр


2
@Dlaor: Ви навіть читали посилання в моєму коментарі? Перший - про C #, а другий - мова-агностик з прийнятою відповіддю, який зосереджується на C #.
gnovice

7
@gnovice, перший запитує про різницю в продуктивності, тоді як я запитав про фактичну різницю коду, другий - про різницю в циклі, тоді як я запитував про різницю в цілому, а третій - про C ++.
Dlaor

1
Я вважаю, що це не дублікат Різниці між i ++ та ++ i в циклі? - як каже Dloar у своєму коментарі вище, інше питання запитує конкретно про використання в циклі.
chue x

Відповіді:


201

Як не дивно, це виглядає так, що два інших відповіді не прописують це, і це, безумовно, варто сказати:


i++означає "скажіть мені значення i, а потім приріст"

++iозначає "приріст i, то скажіть мені значення"


Вони є до-інкрементними, післякріпленими операторами. В обох випадках змінна збільшується , але якщо ви взяли значення обох виразів в абсолютно однакових випадках, результат буде відрізнятися.


11
Здається, це суперечить тому, що говорить Ерік
Еван Керролл

65
Жодне твердження не є правильним. Розглянемо ваше перше твердження. i ++ насправді означає "зберегти значення, збільшити його, зберегти в i, а потім сказати мені початкове збережене значення". Тобто, розповідання відбувається після збільшення, а не раніше, як ви це заявили. Розглянемо друге твердження. i ++ насправді означає "зберегти значення, збільшити його, зберегти в i та сказати мені збільшене значення". Як ви сказали, це незрозуміло, чи є значенням значення i, чи значенням, яке було призначено i; вони могли бути різними .
Ерік Ліпперт

8
@Eric, мені здається навіть з вашою відповіддю, друге твердження категорично правильне. Хоча він виключає кілька кроків.
Еван Керролл

20
Звичайно, більшість випадків неохайні, неправильні способи опису оперативної семантики дають ті самі результати, що і точний і правильний опис. По-перше, я не бачу переконливого значення в отриманні правильних відповідей за допомогою невірних міркувань, а по-друге, так, я бачив виробничий код, який не так точно відповідає. У мене, мабуть, виникає півдесятка запитань на рік від реальних програмістів про те, чому якийсь конкретний виразний фрак, наповнений приростами та зменшеннями та відмінами масиву, не працює так, як вони вважали.
Ерік Ліпперт

12
@supercat: Дискусія стосується C #, а не C.
Thanatos

428

Типова відповідь на це питання, на жаль, вже розміщене тут, полягає в тому, що один робить приріст "до", що залишився, а інший збільшує "після", що залишився. Незважаючи на те, що інтуїтивно зрозуміла ця ідея, це твердження на очах у неї абсолютно неправильне . TheПослідовність подій у часі дуже добре визначені в C #, і це рішуче НЕ той випадок, коли префікс (++ вару) і постфікси (вар ++) версія ++ робити речі в іншому порядку по відношенню до інших операцій.

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

На це потрібно відповісти два питання, щоб визначити, що саме в C # є операцією префікса та постфікса ++. Перше питання - що це за результат?а друге питання - коли відбувається побічний ефект збільшення?

Не очевидно, що відповідь на будь-яке питання, але насправді це досить просто, як тільки ви його бачите. Дозвольте чітко прописати для вас, що x ++ та ++ x роблять для змінної x.

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

  1. x оцінюється для отримання змінної
  2. Значення змінної копіюється у тимчасове місце
  3. Тимчасове значення збільшується для отримання нового значення (не замінюючи тимчасове!)
  4. Нове значення зберігається у змінній
  5. Результатом операції є нове значення (тобто збільшення вартості тимчасового)

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

  1. x оцінюється для отримання змінної
  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операцій, засноване на тимчасовому, а не фактичному значенні, збереженому в змінній у момент, коли будь-яка операція закінчується.

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

Простіше кажучи, версія postfix повертає значення, яке було прочитане зі змінної (тобто значення тимчасового), тоді як версія префікса повертає значення, яке було записане назад, до змінної (тобто збільшення вартості тимчасового). Не повертайте значення змінної.

Це важливо зрозуміти, оскільки сама змінна може бути мінливою і змінитися на іншій потоці, що означає, що повернене значення цих операцій може відрізнятися від поточного значення, що зберігається в змінній.

На диво звичайно, що люди дуже заплутані у перевазі, асоціативності та порядку виконання побічних ефектів, я підозрюю, головним чином, оскільки це так заплутано в C. C # було ретельно розроблено, щоб бути менш заплутаним у всіх цих питаннях. Для додаткового аналізу цих питань, зокрема для подальшого демонстрування помилковості ідеї, що операції з префіксами та постфіксами "переміщують речі в часі", див:

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

що призвело до цього питання ТА:

int [] arr = {0}; int значення = 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 ++ на мовах, подібних С?


4
+1: Для по-справжньому цікавих, чи можете ви дати посилання на те, що ви маєте на увазі під „недоліками дизайну цих операцій в C”?
Джастін Ардіні

21
@Justin: Я додав декілька посилань. Але в основному: у С у вас немає жодних гарантій щодо того, в якому порядку відбуваються речі вчасно. Відповідний компілятор може робити будь-яку прокляту річ, коли йому заманеться, коли в одній точці послідовності є дві мутації, і ніколи не потрібно говорити вам, що ви робите щось, що визначається реалізацією поведінки. Це змушує людей писати небезпечно непереносний код, який працює на деяких компіляторах і робить щось зовсім інше на інших.
Ерік Ліпперт

14
Треба сказати, що для допитливих це хороші знання, але для середнього додатка C # різниця між формулюванням в інших відповідях та фактичними речами, які відбуваються, набагато нижче рівня абстракції мови, який він насправді робить без різниці. C # НЕ асемблер, а з 99,9% часу i++або ++iвикористовуються в коді, речі , що відбуваються в тлі щойно; на задньому плані . Я пишу на C #, щоб піднятися на рівні абстракції вище того, що відбувається на цьому рівні, тому, якщо це дійсно має значення для вашого коду C #, ви, можливо, вже не знаєте мовою.
Томаш Ашан

24
@Tomas: По-перше, я стурбований усіма програмами C #, а не лише масою середніх програм. Кількість нестандартних додатків велика. По-друге, це не має ніякої різниці, крім випадків, коли це має значення . У мене виникають запитання щодо цього матеріалу на основі реальних помилок у реальному коді, ймовірно, півдесятка разів на рік. По-третє, я згоден з вашим більшим моментом; вся ідея ++ - це ідея дуже низького рівня, яка виглядає дуже прискіпливо в сучасному коді. Це насправді лише тому, що для мов, подібних до С, ідентичним є такий оператор.
Ерік Ліпперт

11
+1, але я мушу сказати правду ... Протягом багатьох років я використовую лише оператори Postfix, які я не один в ряду (щоб це не було 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;

Зазвичай той чи інший спосіб не має великого значення і зводиться до стилю кодування, але якщо ви використовуєте операторів всередині інших завдань (як, наприклад, у моїх оригінальних прикладах), важливо знати про можливі побічні ефекти.


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 (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 #, але я припускаю, що це було б.


2
Ось чому я використовую ++ i в c # to, коли використовується сам (тобто не в більшому виразі). Щоразу, коли я бачу цикл ac # з i ++, я трохи плачу, але тепер, коли я знаю, що в c # це насправді майже той самий код, я почуваю себе краще. Особисто я все одно буду використовувати ++ i, тому що звички важко вмирають.
Мікле
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.