Qual è la differenza tra i ++ e ++ i?


204

Li ho visti entrambi in uso in numerosi pezzi di codice C #, e mi piacerebbe sapere quando usare i++o ++i( iessendo una variabile numero come int, float, double, ecc). Qualcuno che lo sa?


13
Tranne dove non fa alcuna differenza, non dovresti mai usare nessuno dei due, perché farà sì che le persone facciano la stessa domanda che stai facendo ora. "Non farmi pensare" si applica al codice e al design.
Istanza Hunter,


2
@Dlaor: hai anche letto i link nel mio commento? Il primo riguarda C # e il secondo è indipendente dalla lingua con una risposta accettata che si concentra su C #.
gnovice,

7
@gnovice, il primo chiede la differenza di prestazioni mentre io ho chiesto la differenza di codice reale, il secondo riguarda la differenza in un ciclo mentre ho chiesto la differenza in generale, e il terzo riguarda C ++.
Dlaor,

1
Credo che questo non sia un duplicato della differenza tra i ++ e ++ i in un ciclo? - come dice Dloar nel suo commento sopra l'altra domanda che pone specificamente sull'uso all'interno di un ciclo.
chue x

Risposte:


201

Stranamente sembra che le altre due risposte non lo spieghino, e vale sicuramente la pena dire:


i++significa "dimmi il valore di i, quindi incrementa"

++isignifica "incremento i, quindi dimmi il valore"


Sono operatori pre-incremento, post-incremento. In entrambi i casi la variabile viene incrementata , ma se si dovesse assumere il valore di entrambe le espressioni esattamente negli stessi casi, il risultato sarà diverso.


11
Questo sembra essere in contrasto con ciò che Eric sta dicendo
Evan Carroll,

65
Nessuna delle due affermazioni è corretta. Considera la tua prima affermazione. i ++ in realtà significa "salva il valore, incrementalo, memorizzalo in i, quindi dimmi il valore originale salvato". Cioè, il racconto avviene dopo l'incremento, non prima come l'hai dichiarato. Considera la seconda affermazione. i ++ in realtà significa "salva il valore, incrementalo, memorizzalo in i e dimmi il valore incrementato". Il modo in cui lo hai detto rende poco chiaro se il valore è il valore di i o il valore assegnato a i; potrebbero essere diversi .
Eric Lippert,

8
@Eric, mi sembrerebbe anche con la tua risposta, la seconda affermazione è categoricamente corretta. Anche se esclude alcuni passaggi.
Evan Carroll,

20
Certo, il più delle volte i modi sciatti e scorretti di descrivere la semantica operativa danno gli stessi risultati della descrizione precisa e corretta. In primo luogo, non vedo alcun valore convincente nell'ottenere le risposte giuste attraverso un ragionamento errato, e in secondo luogo, sì, ho visto il codice di produzione che sbaglia esattamente questo genere di cose. Probabilmente ricevo una mezza dozzina di domande all'anno da veri programmatori sul perché una particolare espressione piena zeppa di incrementi, decrementi e dereferenze di array non funziona come pensavano.
Eric Lippert,

12
@supercat: la discussione riguarda C #, non C.
Thanatos,

428

La risposta tipica a questa domanda, purtroppo già pubblicata qui, è che uno fa l'incremento "prima" delle operazioni rimanenti e l'altro fa l'incremento "dopo" le operazioni rimanenti. Anche se ciò rende intuitivamente l'idea, quella affermazione è completamente sbagliata . La sequenza di eventi nel tempo è estremamente ben definita in C #, e non è enfaticamente no vero che le versioni prefisso (++ var) e postfix (var ++) di ++ facciano le cose in un ordine diverso rispetto ad altre operazioni.

Non sorprende che vedrai molte risposte sbagliate a questa domanda. Molti libri "insegnati a C #" sbagliano anche. Inoltre, il modo in cui C # lo fa è diverso da come lo fa C. Molte persone ragionano come se C # e C fossero la stessa lingua; non sono. La progettazione degli operatori di incremento e decremento in C # secondo me evita i difetti di progettazione di questi operatori in C.

Esistono due domande a cui è necessario rispondere per determinare quali sono esattamente le operazioni di prefisso e postfisso ++ in C #. La prima domanda è: qual è il risultato? e la seconda domanda è quando ha luogo l'effetto collaterale dell'incremento?

Non è ovvio quale sia la risposta a nessuna delle due domande, ma in realtà è abbastanza semplice una volta che la vedi. Lascia che ti spieghi esattamente cosa fanno x ++ e ++ x per una variabile x.

Per il modulo prefisso (++ x):

  1. x viene valutato per produrre la variabile
  2. Il valore della variabile viene copiato in una posizione temporanea
  3. Il valore temporaneo viene incrementato per produrre un nuovo valore (non sovrascrivere il temporaneo!)
  4. Il nuovo valore è memorizzato nella variabile
  5. Il risultato dell'operazione è il nuovo valore (ovvero il valore incrementato del temporaneo)

Per il modulo postfix (x ++):

  1. x viene valutato per produrre la variabile
  2. Il valore della variabile viene copiato in una posizione temporanea
  3. Il valore temporaneo viene incrementato per produrre un nuovo valore (non sovrascrivere il temporaneo!)
  4. Il nuovo valore è memorizzato nella variabile
  5. Il risultato dell'operazione è il valore del temporaneo

Alcune cose da notare:

Innanzitutto, l'ordine degli eventi nel tempo è esattamente lo stesso in entrambi i casi . Ancora, è assolutamente non il caso che l' ordine degli eventi in tempo varia tra prefisso e suffisso. È del tutto falso affermare che la valutazione avviene prima di altre valutazioni o dopo altre valutazioni. Le valutazioni avvengono esattamente nello stesso ordine in entrambi i casi, come si può vedere dai passaggi da 1 a 4 identici. L' unica differenza è l' ultimo passaggio : se il risultato è il valore del valore temporaneo o del nuovo valore incrementato.

Puoi dimostrarlo facilmente con una semplice app per 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);
    }
}

Ecco i risultati ...

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

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

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

Nel primo test, puoi vedere che entrambi currentValuee ciò che è stato passato alTestMethod() all'estensione mostrano lo stesso valore, come previsto.

Tuttavia, nel secondo caso, le persone proveranno a dirti che l'incremento di currentValueavviene dopo la chiamata a TestMethod(), ma come puoi vedere dai risultati, accade prima della chiamata come indicato dal risultato "Corrente: 2".

In questo caso, innanzitutto il valore di currentValueviene memorizzato in un temporaneo. Successivamente, viene memorizzata una versione incrementata di quel valore, currentValuema senza toccare il temporaneo che conserva ancora il valore originale. Finalmente quel temporaneo è passato a TestMethod(). Se l'incremento si è verificato dopo la chiamata a TestMethod(), verrà scritto due volte lo stesso valore non incrementato, ma non lo è.

È importante notare che il valore restituito da entrambe le operazioni currentValue++e ++currentValuesi basa sul valore temporaneo e non sul valore effettivo memorizzato nella variabile al momento in cui entrambe le operazioni terminano.

Richiama nell'ordine delle operazioni sopra, i primi due passaggi copiano il valore corrente della variabile nel temporaneo. Questo è ciò che viene utilizzato per calcolare il valore di ritorno; nel caso della versione del prefisso, è quel valore temporaneo incrementato mentre nel caso della versione del suffisso, è quel valore direttamente / non incrementato. La variabile stessa non viene letta nuovamente dopo la memorizzazione iniziale nel temporaneo.

In parole povere, la versione postfix restituisce il valore letto dalla variabile (ovvero il valore del temporaneo) mentre la versione prefisso restituisce il valore che è stato riscritto nella variabile (ovvero il valore incrementato del temporaneo). Né restituisce il valore della variabile.

Questo è importante da capire perché la variabile stessa potrebbe essere volatile e essere cambiata su un altro thread, il che significa che il valore di ritorno di tali operazioni potrebbe differire dal valore corrente memorizzato nella variabile.

È sorprendentemente comune che le persone si confondano molto con la precedenza, l'associatività e l'ordine in cui vengono eseguiti gli effetti collaterali, sospetto soprattutto perché è così confuso in C. C # è stato attentamente progettato per essere meno confuso in tutti questi aspetti. Per qualche ulteriore analisi di questi problemi, incluso me che dimostri ulteriormente la falsità dell'idea che le operazioni di prefisso e postfisso "spostano roba nel tempo" vedi:

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

che ha portato a questa domanda SO:

int [] arr = {0}; int value = arr [arr [0] ++]; Valore = 1?

Potresti anche essere interessato ai miei precedenti articoli sull'argomento:

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

e

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

e un caso interessante in cui C rende difficile ragionare sulla correttezza:

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

Inoltre, incontriamo problemi simili simili quando consideriamo altre operazioni che hanno effetti collaterali, come semplici incatenamenti:

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

Ed ecco un post interessante sul perché gli operatori di incremento generano valori in C # anziché in variabili :

Perché non riesco a fare ++ i ++ in linguaggi simili a C?


4
+1: Per i veramente curiosi, potresti dare un riferimento a ciò che intendi per "difetti di progettazione di queste operazioni in C"?
Justin Ardini,

21
@Justin: ho aggiunto alcuni link. Ma fondamentalmente: in C, non hai garanzie di sorta sull'ordine in cui le cose accadono nel tempo. Un compilatore conforme può fare qualsiasi cosa dannatamente gradita quando ci sono due mutazioni nello stesso punto di sequenza e non deve mai dirti che stai facendo qualcosa che è un comportamento definito dall'implementazione. Questo porta le persone a scrivere codice pericolosamente non portabile che funziona su alcuni compilatori e fa qualcosa di completamente diverso su altri.
Eric Lippert,

14
Devo dire che per i davvero curiosi, questa è una buona conoscenza, ma per l'applicazione C # media, la differenza tra la formulazione nelle altre risposte e le cose reali in corso è molto al di sotto del livello di astrazione del linguaggio che rende davvero nessuna differenza. C # non è un assemblatore e dal 99,9% delle volte i++o ++iviene utilizzato nel codice, le cose che accadono in background sono proprio questo; sullo sfondo . Scrivo C # per salire ai livelli di astrazione al di sopra di quello che sta succedendo a questo livello, quindi se questo conta davvero per il tuo codice C #, potresti già essere nella lingua sbagliata.
Tomas Aschan,

24
@Tomas: Prima di tutto, sono preoccupato per tutte le applicazioni C #, non solo per la massa di applicazioni medie. Il numero di applicazioni non medie è elevato. In secondo luogo, non fa alcuna differenza se non nei casi in cui fa la differenza . Ricevo domande su queste cose basate su bug reali nel codice reale probabilmente una mezza dozzina di volte all'anno. Terzo, sono d'accordo con il tuo punto più ampio; l'intera idea di ++ è un'idea di livello molto basso che appare molto caratteristica nel codice moderno. È davvero lì solo perché è idiomatico che linguaggi simili a C abbiano un tale operatore.
Eric Lippert,

11
+1 ma devo dire la verità ... Sono anni e anni che l'unico uso degli operatori postfix che faccio che non è solo nella fila (quindi non lo i++;è) è in for (int i = 0; i < x; i++)... E sono molto felice di questo! (e non uso mai l'operatore prefisso). Se devo scrivere qualcosa che richiederà un programmatore senior 2 minuti per decifrare ... Beh ... È meglio scrivere un'altra riga di codice o introdurre una variabile temporanea :-) Penso che il tuo "articolo" (ho vinto "chiamalo" risposta ") conferma la mia scelta :-)
xanatos,

23

Se hai:

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

allora xsarà 11.

Ma se hai:

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

allora xsarà 10.

Nota come sottolinea Eric, l'incremento avviene contemporaneamente in entrambi i casi, ma è il valore che viene dato in quanto il risultato differisce (grazie Eric!).

In generale, mi piace usare a ++imeno che non ci sia una buona ragione per non farlo. Ad esempio, quando scrivo un ciclo, mi piace usare:

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

Oppure, se ho solo bisogno di incrementare una variabile, mi piace usare:

++x;

Normalmente, in un modo o nell'altro non ha molto significato e si riduce allo stile di codifica, ma se si utilizzano gli operatori all'interno di altri compiti (come nei miei esempi originali), è importante essere consapevoli dei potenziali effetti collaterali.


2
Probabilmente potresti chiarire i tuoi esempi mettendo in ordine le righe di codice - vale a dire "var x = 10; var y = ++ x;" e "var x = 10; var y = x ++;" anziché.
Tomas Aschan,

21
Questa risposta è pericolosamente sbagliata. Quando si verifica l'incremento non cambia rispetto alle operazioni rimanenti, quindi dire che in un caso è fatto prima delle operazioni rimanenti e nell'altro caso dopo che le operazioni rimanenti è profondamente fuorviante. L'incremento viene eseguito contemporaneamente in entrambi i casi . La cosa diversa è quale valore viene dato come risultato , non quando viene fatto l'incremento .
Eric Lippert,

3
Ho modificato questo da usare iper il nome della variabile piuttosto che varin quanto è una parola chiave C #.
Jeff Yates,

2
Quindi, senza assegnare variabili e solo affermando "i ++;" o "++ i;" darà esattamente gli stessi risultati o sto sbagliando?
Dlaor,

1
@Eric: potresti chiarire perché la formulazione originale è "pericolosa"? Conduce al corretto utilizzo, anche se i dettagli sono errati.
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.

Questo risponde alla tua domanda?


4
Si potrebbe immaginare che l'incremento postfisso e il decremento prefisso non abbiano alcun effetto sulla variabile: P
Andreas,

5

Il modo in cui funziona l'operatore è che viene incrementato contemporaneamente, ma se è prima di una variabile, l'espressione valuterà con la variabile incrementata / decrementata:

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

Se è dopo la variabile, l'istruzione corrente verrà eseguita con la variabile originale, come se non fosse stata ancora incrementata / decrementata:

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

Concordo con dcp nell'uso del pre-incremento / decremento (++ x) se non necessario. Davvero l'unica volta in cui uso il post-incremento / decremento è in loop o loop di quel tipo. Questi loop sono gli stessi:

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

o

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

Puoi anche farlo indicizzando matrici e simili:

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

Ecc, ecc ...


4

Solo per la cronaca, in C ++, se puoi usare uno dei due (cioè) non ti interessa l'ordine delle operazioni (vuoi solo incrementare o decrementare e usarlo in seguito) l'operatore prefisso è più efficiente poiché non lo fa devi creare una copia temporanea dell'oggetto. Sfortunatamente, la maggior parte delle persone usa posfix (var ++) invece del prefisso (++ var), proprio perché è quello che abbiamo imparato inizialmente. (Mi è stato chiesto di questo in un'intervista). Non sono sicuro che ciò sia vero in C #, ma suppongo che lo sarebbe.


2
Ecco perché uso ++ i in c # quando usato da solo (cioè non in un'espressione più grande). Ogni volta che vedo ac # loop con i ++, piango un po ', ma ora che so che in c # questo è praticamente lo stesso codice mi sento meglio. Personalmente userò ancora ++ i perché le abitudini sono dure a morire.
Mikle,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.