¿Cuál es la diferencia entre i ++ y ++ i?

204

Los he visto tanto que se utilizan en numerosos trozos de código C #, y me gustaría saber cuándo usar i++o ++i( isiendo una variable igual número int, float, double, etc). ¿Alguien que sepa esto?

Dlaor
fuente
13
Excepto donde no haya ninguna diferencia, nunca debe usar ninguno de los dos, ya que solo hará que las personas hagan la misma pregunta que usted hace ahora. "No me hagas pensar" se aplica tanto al código como al diseño.
Instancia Hunter
2
@Dlaor: ¿Leíste los enlaces en mi comentario? El primero es sobre C #, y el segundo es independiente del lenguaje con una respuesta aceptada que se centra en C #.
gnovice
77
@gnovice, el primero pregunta sobre la diferencia de rendimiento mientras que yo pregunté sobre la diferencia de código real, el segundo es sobre la diferencia en un bucle mientras que pregunté la diferencia en general, y el tercero es sobre C ++.
Dlaor
1
¿Creo que esto no es un duplicado de la diferencia entre i ++ y ++ i en un bucle? - como Dloar dice en su comentario anterior, la otra pregunta se refiere específicamente al uso dentro de un bucle.
chue x

Respuestas:

201

Curiosamente parece que las otras dos respuestas no lo explican, y definitivamente vale la pena decir:


i++significa 'dime el valor de i, luego incrementa'

++isignifica 'incrementar i, entonces dime el valor'


Son operadores de pre-incremento, post-incremento. En ambos casos, la variable se incrementa , pero si tuviera que tomar el valor de ambas expresiones exactamente en los mismos casos, el resultado sería diferente.

Kieren Johnstone
fuente
11
Esto parece estar en desacuerdo con lo que Eric está diciendo
Evan Carroll
65
Ninguna de las afirmaciones es correcta. Considera tu primera declaración. i ++ en realidad significa "guardar el valor, incrementarlo, almacenarlo en i, luego decirme el valor original guardado". Es decir, la narración ocurre después del incremento, no antes como lo has dicho. Considere la segunda declaración. i ++ en realidad significa "guardar el valor, incrementarlo, almacenarlo en i y decirme el valor incrementado". La forma en que lo dijo deja en claro si el valor es el valor de i, o el valor que se le asignó a i; Podrían ser diferentes .
Eric Lippert
8
@ Eric, me parecería incluso con tu respuesta, la segunda afirmación es categóricamente correcta. Aunque excluye algunos pasos.
Evan Carroll
20
Claro, la mayoría de las veces las formas descuidadas e incorrectas de describir la semántica operativa dan los mismos resultados que la descripción precisa y correcta. Primero, no veo ningún valor convincente para obtener las respuestas correctas a través de un razonamiento incorrecto, y segundo, sí, he visto un código de producción que hace exactamente este tipo de cosas mal. Probablemente recibo media docena de preguntas al año de programadores reales sobre por qué alguna expresión en particular repleta de incrementos y decrementos y desreferencias de matriz no funciona de la manera que asumieron.
Eric Lippert
12
@supercat: La discusión es sobre C #, no C.
Thanatos
428

La respuesta típica a esta pregunta, desafortunadamente publicada aquí, es que uno hace el incremento "antes" de las operaciones restantes y el otro hace el incremento "después" de las operaciones restantes. Aunque eso intuitivamente transmite la idea, esa afirmación es completamente incorrecta . La secuencia de eventos en el tiempo está extremadamente bien definida en C #, y enfáticamente no el caso que las versiones de prefijo (++ var) y postfix (var ++) de ++ hagan las cosas en un orden diferente con respecto a otras operaciones.

No es sorprendente que vea muchas respuestas incorrectas a esta pregunta. Una gran cantidad de libros de "autodidacta C #" también se equivocan. Además, la forma en que C # lo hace es diferente de cómo lo hace C. Mucha gente razona como si C # y C fueran el mismo lenguaje; ellos no son. El diseño de los operadores de incremento y decremento en C # en mi opinión evita las fallas de diseño de estos operadores en C.

Hay dos preguntas que deben responderse para determinar cuál es exactamente la operación de prefijo y postfix ++ en C #. La primera pregunta es ¿cuál es el resultado? y la segunda pregunta es ¿ cuándo tiene lugar el efecto secundario del incremento?

No es obvio cuál es la respuesta a cualquiera de las preguntas, pero en realidad es bastante simple una vez que la ve. Permíteme explicarte con precisión qué hacen x ++ y ++ x para una variable x.

Para la forma del prefijo (++ x):

  1. x se evalúa para producir la variable
  2. El valor de la variable se copia en una ubicación temporal.
  3. El valor temporal se incrementa para producir un nuevo valor (¡no sobrescribir el temporal!)
  4. El nuevo valor se almacena en la variable
  5. El resultado de la operación es el nuevo valor (es decir, el valor incrementado del temporal)

Para el formulario de postfix (x ++):

  1. x se evalúa para producir la variable
  2. El valor de la variable se copia en una ubicación temporal.
  3. El valor temporal se incrementa para producir un nuevo valor (¡no sobrescribir el temporal!)
  4. El nuevo valor se almacena en la variable
  5. El resultado de la operación es el valor de lo temporal.

Algunas cosas para notar:

Primero, el orden de los eventos en el tiempo es exactamente el mismo en ambos casos . Una vez más, es absolutamente no el caso de que el orden de los acontecimientos en el tiempo cambia entre el prefijo y de sufijo. Es completamente falso decir que la evaluación ocurre antes de otras evaluaciones o después de otras evaluaciones. Las evaluaciones suceden exactamente en el mismo orden en ambos casos, como puede ver en los pasos 1 a 4 que son idénticos. La única diferencia es el último paso. : si el resultado es el valor del valor incremental temporal o el nuevo.

Puede demostrar esto fácilmente con una sencilla aplicación de consola 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);
    }
}

Aquí están los resultados...

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

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

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

En la primera prueba, puede ver que ambos currentValuey lo que se pasó aTestMethod() extensión muestran el mismo valor, como se esperaba.

Sin embargo, en el segundo caso, las personas tratarán de decirle que el incremento de currentValueocurre después de la llamada a TestMethod(), pero como puede ver en los resultados, sucede antes de la llamada como lo indica el resultado 'Actual: 2'.

En este caso, primero el valor de currentValuese almacena en un temporal. A continuación, una versión incrementada de ese valor se almacena de nuevo currentValuepero sin tocar el temporal que aún almacena el valor original. Finalmente ese temporal se pasa a TestMethod(). Si el incremento ocurrió después de la llamada aTestMethod() entonces escribiría el mismo valor no incrementado dos veces, pero no es así.

Es importante tener en cuenta que el valor devuelto por ambos currentValue++y++currentValue operaciones se basa en el valor temporal y no en el valor real almacenado en la variable en el momento en que sale cualquiera de las operaciones.

Recuerde en el orden de las operaciones anteriores, los dos primeros pasos copian el valor actual de la variable en el temporal. Eso es lo que se usa para calcular el valor de retorno; en el caso de la versión de prefijo, es ese valor temporal incrementado, mientras que en el caso de la versión de sufijo, es ese valor directamente / no incrementado. La variable en sí misma no se vuelve a leer después del almacenamiento inicial en el temporal.

En pocas palabras, la versión de postfix devuelve el valor que se leyó de la variable (es decir, el valor de lo temporal) mientras que la versión de prefijo devuelve el valor que se volvió a escribir en la variable (es decir, el valor incrementado de lo temporal). Tampoco devuelve el valor de la variable.

Esto es importante de entender porque la variable en sí misma podría ser volátil y haber cambiado en otro subproceso, lo que significa que el valor de retorno de esas operaciones podría diferir del valor actual almacenado en la variable.

Es sorprendentemente común que las personas se confundan mucho sobre la precedencia, la asociatividad y el orden en que se ejecutan los efectos secundarios, sospecho principalmente porque es muy confuso en C. C # ha sido cuidadosamente diseñado para ser menos confuso en todos estos aspectos. Para un análisis adicional de estos problemas, incluyéndome para demostrar aún más la falsedad de la idea de que las operaciones de prefijos y postfijos "mueven cosas a tiempo", vea:

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

lo que llevó a esta pregunta SO:

int [] arr = {0}; valor int = arr [arr [0] ++]; Valor = 1?

También te pueden interesar mis artículos anteriores sobre el tema:

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

y

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

y un caso interesante en el que C dificulta razonar sobre la corrección:

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

Además, nos encontramos con problemas sutiles similares al considerar otras operaciones que tienen efectos secundarios, como asignaciones simples encadenadas:

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

Y aquí hay una publicación interesante sobre por qué los operadores de incremento resultan en valores en C # en lugar de en variables :

¿Por qué no puedo hacer ++ i ++ en lenguajes tipo C?

Eric Lippert
fuente
44
+1: Para los verdaderamente curiosos, ¿podría dar una referencia de lo que quiere decir con los "defectos de diseño de estas operaciones en C"?
Justin Ardini
21
@Justin: He agregado algunos enlaces. Pero básicamente: en C, no tienes garantías sobre el orden en que suceden las cosas a tiempo. Un compilador conforme puede hacer cualquier cosa que quiera cuando hay dos mutaciones en el mismo punto de secuencia, y nunca necesita decirle que está haciendo algo que es un comportamiento definido por la implementación. Esto lleva a las personas a escribir código peligrosamente no portátil que funciona en algunos compiladores y hace algo completamente diferente en otros.
Eric Lippert
14
Debo decir que para los realmente curiosos, este es un buen conocimiento, pero para la aplicación promedio de C #, la diferencia entre la redacción en las otras respuestas y las cosas reales está muy por debajo del nivel de abstracción del lenguaje que realmente hace ninguna diferencia. C # no es ensamblador, y del 99.9% de las veces i++o ++ise usan en código, las cosas que están sucediendo en segundo plano son solo eso; en el fondo . Escribo C # para subir a niveles de abstracción por encima de lo que está sucediendo en este nivel, por lo que si esto realmente importa para su código C #, es posible que ya esté en el lenguaje incorrecto.
Tomas Aschan
24
@Tomas: En primer lugar, me preocupan todas las aplicaciones de C #, no solo la masa de las aplicaciones promedio. El número de aplicaciones no promedio es grande. En segundo lugar, no hace ninguna diferencia, excepto en los casos en que hace la diferencia . Recibo preguntas sobre estas cosas basadas en errores reales en código real, probablemente media docena de veces al año. Tercero, estoy de acuerdo con tu punto más amplio; toda la idea de ++ es una idea de muy bajo nivel que se ve muy pintoresca en el código moderno. Realmente solo está allí porque es idiomático que los lenguajes tipo C tengan dicho operador.
Eric Lippert
11
+1 pero tengo que decir la verdad ... Son años y años que el único uso de operadores de postfix que hago que no está solo en la fila (por lo que no i++;) está en for (int i = 0; i < x; i++)... Y estoy muy muy feliz de esto! (y nunca uso el operador de prefijo). Si tengo que escribir algo que requerirá que un programador senior descifre 2 minutos ... Bueno ... Es mejor escribir una línea más de código o introducir una variable temporal :-) Creo que su "artículo" (gané no lo llamo "respuesta") reivindica mi elección :-)
xanatos
23

Si usted tiene:

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

entonces xserá 11.

Pero si tienes:

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

entonces xserá 10.

Tenga en cuenta que, como señala Eric, el incremento se produce al mismo tiempo en ambos casos, pero es el valor que se da como resultado lo que difiere (¡gracias Eric!).

En general, me gusta usar a ++imenos que haya una buena razón para no hacerlo. Por ejemplo, cuando escribo un bucle, me gusta usar:

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

O, si solo necesito incrementar una variable, me gusta usar:

++x;

Normalmente, de una forma u otra no tiene mucha importancia y se reduce al estilo de codificación, pero si está utilizando los operadores dentro de otras tareas (como en mis ejemplos originales), es importante tener en cuenta los posibles efectos secundarios.

dcp
fuente
2
Probablemente podría aclarar sus ejemplos ordenando las líneas de código, es decir, "var x = 10; var y = ++ x;" y "var x = 10; var y = x ++;" en lugar.
Tomas Aschan
21
Esta respuesta es peligrosamente incorrecta. Cuando ocurre el incremento no cambia con respecto a las operaciones restantes, por lo que decir que en un caso se realiza antes de las operaciones restantes y en el otro caso se realiza después de que las operaciones restantes sean profundamente engañosas. El incremento se realiza al mismo tiempo en ambos casos . Lo que es diferente es qué valor se da como resultado , no cuando se realiza el incremento .
Eric Lippert
3
He editado esto para usarlo ipara el nombre de la variable en lugar de varcomo es una palabra clave de C #.
Jeff Yates
2
Entonces, sin asignar variables y simplemente declarando "i ++;" o "++ i;" dará exactamente los mismos resultados o me estoy equivocando?
Dlaor
1
@Eric: ¿Podría aclarar por qué la redacción original es "peligrosa"? Conduce al uso correcto, incluso si los detalles son incorrectos.
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.

¿Responde esto a tu pregunta?

el perdido
fuente
44
Uno podría imaginar que el incremento de postfix y la disminución de prefijo no tienen ningún efecto en la variable: P
Andreas
5

La forma en que funciona el operador es que se incrementa al mismo tiempo, pero si está antes de una variable, la expresión se evaluará con la variable incrementada / decrementada:

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

Si es posterior a la variable, la instrucción actual se ejecutará con la variable original, como si aún no se hubiera incrementado / decrementado:

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

Estoy de acuerdo con dcp en el uso de pre-incremento / decremento (++ x) a menos que sea necesario. Realmente, la única vez que uso el incremento / decremento posterior es en bucles while o bucles de ese tipo. Estos bucles son iguales:

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
}

También puede hacer esto mientras indexa matrices y demás:

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étera etcétera...

Mike Webb
fuente
4

Solo para el registro, en C ++, si puede usar cualquiera de ellos (es decir, no le importa el orden de las operaciones (solo desea aumentar o disminuir y usarlo más adelante), el operador de prefijo es más eficiente ya que no tiene que crear una copia temporal del objeto. Desafortunadamente, la mayoría de las personas usan posfix (var ++) en lugar de prefijo (++ var), solo porque eso es lo que aprendimos inicialmente. (Me preguntaron sobre esto en una entrevista). No estoy seguro de si esto es cierto en C #, pero supongo que lo sería.

DevinEllingson
fuente
2
Es por eso que uso ++ i en c # cuando se usa solo (es decir, no en una expresión más grande). Cada vez que veo un bucle ac # con i ++, lloro un poco, pero ahora que sé que en c # este es realmente el mismo código, me siento mejor. Personalmente, seguiré usando ++ i porque los hábitos son muy difíciles.
Mikle