Quelle est la différence entre i ++ et ++ i?


204

Je les ai vus tous deux utilisés dans de nombreux morceaux de code C #, et je voudrais savoir quand utiliser i++ou ++i( iétant une variable de nombre comme int, float, double, etc.). Quelqu'un qui sait cela?


13
Sauf là où cela ne fait aucune différence, vous ne devriez jamais utiliser l'un ou l'autre, car cela va simplement amener les gens à poser la même question que vous posez maintenant. "Ne me faites pas penser" s'applique aussi bien au code qu'au design.
Instance Hunter


2
@Dlaor: Avez-vous même lu les liens dans mon commentaire? Le premier concerne le C #, et le second est indépendant du langage avec une réponse acceptée qui se concentre sur le C #.
gnovice

7
@gnovice, le premier pose des questions sur la différence de performances alors que je pose des questions sur la différence réelle au niveau du code, le second porte sur la différence dans une boucle tandis que je pose la différence en général, et le troisième sur C ++.
Dlaor

1
Je crois que ce n'est pas un doublon de différence entre i ++ et ++ i dans une boucle? - comme le dit Dloar dans son commentaire ci - dessus, l'autre question concerne spécifiquement l'utilisation dans une boucle.
chue x

Réponses:


201

Curieusement, il semble que les deux autres réponses ne l'expliquent pas, et cela vaut vraiment la peine de dire:


i++ signifie "dites-moi la valeur de i , puis incrémentez"

++isignifie «incrémenter i, puis dites-moi la valeur»


Ce sont des opérateurs pré-incrément, post-incrément. Dans les deux cas, la variable est incrémentée , mais si vous deviez prendre la valeur des deux expressions exactement dans les mêmes cas, le résultat sera différent.


11
Cela semble en contradiction avec ce que dit Eric
Evan Carroll

65
Aucune de ces affirmations n'est correcte. Considérez votre première déclaration. i ++ signifie en fait "enregistrer la valeur, l'incrémenter, la stocker dans i, puis me dire la valeur enregistrée d'origine". Autrement dit, le récit se produit après l'incrémentation, pas avant comme vous l'avez dit. Considérez la deuxième déclaration. i ++ signifie en fait "enregistrer la valeur, l'incrémenter, la stocker dans i et me dire la valeur incrémentée". La façon dont vous l'avez dit ne permet pas de savoir clairement si la valeur est la valeur de i ou la valeur qui a été affectée à i; ils pourraient être différents .
Eric Lippert

8
@Eric, il me semble que même avec votre réponse, la deuxième affirmation est catégoriquement correcte. Bien qu'il exclue quelques étapes.
Evan Carroll

20
Bien sûr, la plupart du temps , les manières bâclées et incorrectes de décrire la sémantique opérationnelle donnent les mêmes résultats que la description précise et correcte. Premièrement, je ne vois aucune valeur convaincante pour obtenir les bonnes réponses par un raisonnement incorrect, et deuxièmement, oui, j'ai vu du code de production qui se trompe exactement sur ce genre de chose. Je reçois probablement une demi-douzaine de questions par an de vrais programmeurs sur la raison pour laquelle une expression particulière remplie d'incréments et de décréments et de déréférences de tableau ne fonctionne pas comme ils le pensaient.
Eric Lippert

12
@supercat: La discussion porte sur C #, pas sur C.
Thanatos

428

La réponse typique à cette question, malheureusement déjà affichée ici, est que l'un fait l'incrémentation "avant" les opérations restantes et l'autre fait l'incrémentation "après" les opérations restantes. Bien que cela fasse intuitivement passer l'idée, cette déclaration est à première vue complètement fausse . La séquence d'événements dans le temps est extrêmement bien définie en C #, et ce n'est absolument pas vrai que les versions préfixe (++ var) et postfix (var ++) de ++ fassent les choses dans un ordre différent par rapport aux autres opérations.

Il n'est pas surprenant que vous verrez beaucoup de mauvaises réponses à cette question. Un grand nombre de livres «apprenez-vous en C #» se trompent également. De plus, la façon dont C # le fait est différente de la façon dont C le fait. Beaucoup de gens pensent que C # et C sont le même langage; ils ne sont pas. À mon avis, la conception des opérateurs d'incrémentation et de décrémentation en C # évite les défauts de conception de ces opérateurs en C.

Il y a deux questions auxquelles il faut répondre pour déterminer exactement le fonctionnement du préfixe et du suffixe ++ en C #. La première question est quel est le résultat? et la deuxième question est quand se produit l'effet secondaire de l'augmentation?

La réponse à l'une ou l'autre des questions n'est pas évidente, mais elle est en fait assez simple une fois que vous la voyez. Permettez-moi de vous expliquer précisément ce que x ++ et ++ x font pour une variable x.

Pour le préfixe (++ x):

  1. x est évalué pour produire la variable
  2. La valeur de la variable est copiée dans un emplacement temporaire
  3. La valeur temporaire est incrémentée pour produire une nouvelle valeur (sans écraser la valeur temporaire!)
  4. La nouvelle valeur est stockée dans la variable
  5. Le résultat de l'opération est la nouvelle valeur (c'est-à-dire la valeur incrémentée du temporaire)

Pour le formulaire postfix (x ++):

  1. x est évalué pour produire la variable
  2. La valeur de la variable est copiée dans un emplacement temporaire
  3. La valeur temporaire est incrémentée pour produire une nouvelle valeur (sans écraser la valeur temporaire!)
  4. La nouvelle valeur est stockée dans la variable
  5. Le résultat de l'opération est la valeur du

Quelques points à noter:

Premièrement, l'ordre des événements dans le temps est exactement le même dans les deux cas . Encore une fois, il n'est absolument pas vrai que l' ordre des événements dans le temps change entre le préfixe et le postfix. Il est tout à fait faux de dire que l'évaluation a lieu avant d'autres évaluations ou après d'autres évaluations. Les évaluations se déroulent exactement dans le même ordre dans les deux cas, comme vous pouvez le voir aux étapes 1 à 4 étant identiques. La seule différence est la dernière étape - que le résultat soit la valeur de la valeur temporaire ou de la nouvelle valeur incrémentée.

Vous pouvez facilement le démontrer avec une simple application console 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);
    }
}

Voici les résultats...

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

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

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

Dans le premier test, vous pouvez voir que les deux currentValueet ce qui a été transmis auTestMethod() extension affichent la même valeur, comme prévu.

Cependant, dans le second cas, les gens essaieront de vous dire que l'incrémentation currentValuese produit après l'appel à TestMethod(), mais comme vous pouvez le voir sur les résultats, cela se produit avant l'appel, comme indiqué par le résultat 'Current: 2'.

Dans ce cas, la valeur de currentValueest d'abord stockée dans un fichier temporaire. Ensuite, une version incrémentée de cette valeur est stockée dans currentValuemais sans toucher au temporaire qui stocke toujours la valeur d'origine. Enfin, ce temporaire est transmis à TestMethod(). Si l'incrément s'est produit après l'appel à, TestMethod()il écrirait deux fois la même valeur non incrémentée, mais ce n'est pas le cas.

Il est important de noter que la valeur renvoyée par les opérations currentValue++et ++currentValueest basée sur la valeur temporaire et non sur la valeur réelle stockée dans la variable au moment où l'une des opérations se termine.

Rappelez-vous dans l'ordre des opérations ci-dessus, les deux premières étapes copient la valeur alors actuelle de la variable dans le temporaire. C'est ce qui est utilisé pour calculer la valeur de retour; dans le cas de la version préfixe, c'est cette valeur temporaire incrémentée tandis que dans le cas de la version suffixe, c'est cette valeur directement / non incrémentée. La variable elle-même n'est pas relue après le stockage initial dans le temporaire.

En termes plus simples, la version postfixe renvoie la valeur qui a été lue à partir de la variable (c'est-à-dire la valeur du temporaire) tandis que la version du préfixe renvoie la valeur qui a été réécrite dans la variable (c'est-à-dire la valeur incrémentée du temporaire). Ni retourner la valeur de la variable.

Ceci est important à comprendre car la variable elle-même peut être volatile et avoir changé sur un autre thread, ce qui signifie que la valeur de retour de ces opérations peut différer de la valeur actuelle stockée dans la variable.

Il est étonnamment courant que les gens soient très confus au sujet de la priorité, de l'associativité et de l'ordre dans lequel les effets secondaires sont exécutés, je soupçonne principalement parce que c'est tellement déroutant en C.C # a été soigneusement conçu pour être moins déroutant à tous ces égards. Pour une analyse supplémentaire de ces problèmes, y compris moi démontrant davantage la fausseté de l'idée que les opérations de préfixe et de postfix "déplacent les choses dans le temps", voir:

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

qui a conduit à cette question SO:

int [] arr = {0}; valeur int = arr [arr [0] ++]; Valeur = 1?

Vous pourriez également être intéressé par mes précédents articles sur le sujet:

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

et

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

et un cas intéressant où C rend difficile de raisonner sur l'exactitude:

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

En outre, nous rencontrons des problèmes subtils similaires lorsque nous considérons d'autres opérations qui ont des effets secondaires, telles que des affectations simples chaînées:

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

Et voici un article intéressant sur la raison pour laquelle les opérateurs d'incrémentation produisent des valeurs en C # plutôt qu'en variables :

Pourquoi ne puis-je pas faire ++ i ++ dans des langages de type C?


4
+1: Pour les véritables curieux, pourriez-vous donner une référence à ce que vous entendez par les "défauts de conception de ces opérations en C"?
Justin Ardini

21
@Justin: J'ai ajouté quelques liens. Mais en gros: en C, vous n'avez aucune garantie quant à l'ordre dans lequel les choses se produisent dans le temps. Un compilateur conforme peut faire n'importe quoi ce qu'il veut quand il y a deux mutations dans le même point de séquence, et n'a jamais besoin de vous dire que vous faites quelque chose qui est un comportement défini par l'implémentation. Cela conduit les gens à écrire du code dangereusement non portable qui fonctionne sur certains compilateurs et fait quelque chose de complètement différent sur d'autres.
Eric Lippert

14
Je dois dire que pour les très curieux, c'est une bonne connaissance, mais pour l'application C # moyenne, la différence entre le libellé dans les autres réponses et les choses en cours est tellement en dessous du niveau d'abstraction du langage qu'il fait vraiment aucune différence. C # n'est pas assembleur, et sur 99,9% des fois i++ou ++isont utilisés dans du code, les choses qui se passent en arrière-plan ne sont que cela; en arrière-plan . J'écris C # pour grimper à des niveaux d'abstraction supérieurs à ce qui se passe à ce niveau, donc si cela compte vraiment pour votre code C #, vous êtes peut-être déjà dans le mauvais langage.
Tomas Aschan

24
@Tomas: Tout d'abord, je suis préoccupé par toutes les applications C #, pas seulement par la masse des applications moyennes. Le nombre d'applications non moyennes est important. Deuxièmement, cela ne fait aucune différence, sauf dans les cas où cela fait une différence . Je reçois des questions sur ces trucs basés sur de vrais bugs dans du vrai code probablement une demi-douzaine de fois par an. Troisièmement, je suis d'accord avec votre argument plus large; l'idée de ++ est une idée de très bas niveau qui semble très pittoresque dans le code moderne. Ce n'est vraiment là que parce qu'il est idiomatique pour les langages de type C d'avoir un tel opérateur.
Eric Lippert

11
+1 mais je dois dire la vérité ... Cela fait des années et des années que la seule utilisation d'opérateurs postfix que je fais qui n'est pas seule dans la rangée (donc ce n'est pas le cas i++;) est dans for (int i = 0; i < x; i++)... Et je suis très très heureux de ça! (et je n'utilise jamais l'opérateur de préfixe). Si je dois écrire quelque chose qui nécessitera un programmeur senior 2 minutes pour déchiffrer ... Eh bien ... Il vaut mieux écrire une autre ligne de code ou introduire une variable temporaire :-) Je pense que votre "article" (j'ai gagné n'appelle pas ça "répondre") confirme mon choix :-)
xanatos

23

Si tu as:

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

puis xsera 11.

Mais si vous avez:

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

puis xsera 10.

Notez qu'Eric le fait remarquer, l'incrément se produit en même temps dans les deux cas, mais c'est la valeur donnée comme résultat qui diffère (merci Eric!).

En général, j'aime utiliser à ++imoins qu'il y ait une bonne raison de ne pas le faire. Par exemple, lors de l'écriture d'une boucle, j'aime utiliser:

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

Ou, si j'ai juste besoin d'incrémenter une variable, j'aime utiliser:

++x;

Normalement, d'une manière ou d'une autre, cela n'a pas beaucoup d'importance et se résume au style de codage, mais si vous utilisez les opérateurs dans d'autres affectations (comme dans mes exemples originaux), il est important d'être conscient des effets secondaires potentiels.


2
Vous pourriez probablement clarifier vos exemples en mettant les lignes de code dans l'ordre - c'est-à-dire, "var x = 10; var y = ++ x;" et "var x = 10; var y = x ++;" au lieu.
Tomas Aschan

21
Cette réponse est dangereusement fausse. Lorsque l'incrément se produit ne change pas par rapport aux opérations restantes, donc dire que dans un cas, il est fait avant les opérations restantes et dans l'autre cas, il est fait après les opérations restantes est profondément trompeur. L'incrémentation se fait en même temps dans les deux cas . Ce qui est différent, c'est quelle valeur est donnée comme résultat , pas quand l'incrémentation est effectuée .
Eric Lippert

3
J'ai modifié cela pour utiliser ile nom de variable plutôt quevar comme c'est un mot clé C #.
Jeff Yates

2
Donc, sans affecter de variables et en indiquant simplement "i ++;" ou "++ i;" donnera exactement les mêmes résultats ou je me trompe?
Dlaor

1
@Eric: Pourriez-vous préciser pourquoi le libellé d'origine est "dangereux"? Cela conduit à une utilisation correcte, même si les détails sont erronés.
Justin Ardini

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.

Est-ce que cela répond à votre question ?


4
On pourrait imaginer que l'incrémentation postfixe et la décrémentation du préfixe n'ont aucun effet sur la variable: P
Andreas

5

La façon dont l'opérateur fonctionne est qu'il est incrémenté en même temps, mais s'il est avant une variable, l'expression sera évaluée avec la variable incrémentée / décrémentée:

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

Si c'est après la variable, l'instruction courante sera exécutée avec la variable d'origine, comme si elle n'avait pas encore été incrémentée / décrémentée:

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

Je suis d'accord avec dcp pour utiliser la pré-incrémentation / décrémentation (++ x) sauf si nécessaire. Vraiment, la seule fois où j'utilise le post-incrément / décrément, c'est pendant que des boucles ou des boucles de ce type. Ces boucles sont les mêmes:

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

ou

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

Vous pouvez également le faire lors de l'indexation des tableaux et autres:

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);

Etc...


4

Juste pour mémoire, en C ++, si vous pouvez utiliser soit (c'est-à-dire) vous ne vous souciez pas de l'ordre des opérations (vous voulez juste incrémenter ou décrémenter et l'utiliser plus tard), l'opérateur de préfixe est plus efficace car il ne le fait pas doivent créer une copie temporaire de l'objet. Malheureusement, la plupart des gens utilisent posfix (var ++) au lieu du préfixe (++ var), simplement parce que c'est ce que nous avons appris au départ. (On m'a posé des questions à ce sujet dans une interview). Je ne sais pas si cela est vrai en C #, mais je suppose que ce serait le cas.


2
C'est pourquoi j'utilise ++ i en c # pour être utilisé seul (c'est-à-dire pas dans une expression plus grande). Chaque fois que je vois une boucle ac # avec i ++, je pleure un peu, mais maintenant que je sais qu'en c # c'est en fait à peu près le même code, je me sens mieux. Personnellement, j'utiliserai toujours ++ i car les habitudes ont la vie dure.
Mikle
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.