una - Extraño comportamiento de incremento en C#
operador decremento c (6)
Es instructivo ver exactamente dónde está su error:
el lado derecho de una asignación siempre debe evaluarse antes de que su valor se asigne a la variable del lado izquierdo
Correcto. Claramente, el efecto secundario de la asignación no puede ocurrir hasta que se haya calculado el valor asignado.
las operaciones de incremento como ++ y - siempre se realizan justo después de la evaluación
Casi correcto. No está claro qué quiere decir con "evaluación": ¿evaluación de qué? ¿El valor original, el valor incrementado o el valor de la expresión? La forma más fácil de pensarlo es que se calcula el valor original, luego el valor incrementado y luego se produce el efecto secundario. Luego, el valor final es que se elige uno de los valores original o incrementado, dependiendo de si el operador fue prefijo o postfijo. Pero su premisa básica es bastante buena: que el efecto secundario del incremento ocurre inmediatamente después de que se determina el valor final, y luego se produce el valor final.
Entonces parece que está concluyendo una falsedad de estas dos premisas correctas, a saber, que los efectos secundarios del lado izquierdo se producen después de la evaluación del lado derecho. ¡Pero nada en esas dos premisas implica esta conclusión! Acabas de sacar esa conclusión del aire.
Sería más claro si declarara una tercera premisa correcta:
la ubicación de almacenamiento asociada con la variable del lado izquierdo también debe ser conocida antes de que tenga lugar la asignación.
Claramente esto es cierto. Debe saber dos cosas antes de que pueda ocurrir una asignación: qué valor se está asignando y qué ubicación de memoria se está mutando. No puedes resolver esas dos cosas al mismo tiempo; Primero debe averiguar uno de ellos, y nosotros encontraremos uno en el lado izquierdo, la variable, primero en C #. Si averiguar dónde está ubicado el almacenamiento causa un efecto secundario, entonces ese efecto secundario se produce antes de que resolvamos la segunda cosa: el valor que se asigna a la variable.
En resumen, en C #, el orden de las evaluaciones en una asignación a una variable es el siguiente:
- Los efectos secundarios del lado izquierdo pasan y se produce una variable.
- Los efectos secundarios del lado derecho suceden y se produce un valor.
- el valor se convierte implícitamente al tipo del lado izquierdo, que puede producir un tercer efecto secundario
- el efecto secundario de la asignación (la mutación de la variable para que tenga el valor del tipo correcto) ocurre, y se produce un valor, el valor que se acaba de asignar al lado izquierdo.
Nota: tenga en cuenta que el siguiente código es esencialmente sin sentido, y solo con fines ilustrativos.
Basado en el hecho de que el lado derecho de una asignación siempre debe evaluarse antes de que su valor se asigne a la variable del lado izquierdo, y que las operaciones de incremento como ++
y --
siempre se realicen justo después de la evaluación, me gustaría No esperes que el siguiente código funcione:
string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];
int IndTmp = 0;
foreach (string TmpString in newArray1)
{
newArray2[IndTmp] = newArray1[IndTmp++];
}
Más bien, esperaría que newArray1[0]
se asigne a newArray2[1]
, newArray1[1]
a newArray[2]
y así sucesivamente hasta el punto de lanzar una System.IndexOutOfBoundsException
. En cambio, y para mi gran sorpresa, la versión que arroja la excepción es
string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];
int IndTmp = 0;
foreach (string TmpString in newArray1)
{
newArray2[IndTmp++] = newArray1[IndTmp];
}
Como, según tengo entendido, el compilador primero evalúa el RHS, lo asigna al LHS y solo entonces incrementa esto para mí como un comportamiento inesperado. ¿O es realmente esperado y claramente me estoy perdiendo algo?
Esto está bien definido en el lenguaje C # según Eric Lippert y se explica fácilmente.
- Se evalúan las primeras cosas de la expresión de la izquierda que deben ser referenciadas y recordadas, y se tienen en cuenta los efectos secundarios
- Entonces se hace la expresión de orden correcto
Nota: la ejecución real del código podría no ser así, lo importante a recordar es que el compilador debe crear un código que sea equivalente a este
Entonces, lo que sucede en la segunda pieza de código es esto:
- Lado izquierdo:
-
newArray2
se evalúa y el resultado se recuerda (es decir, se recuerda la referencia a la matriz en la que queremos almacenar cosas, en caso de que los efectos secundarios lo cambien más adelante) - Se evalúa
IndTemp
y se recuerda el resultado. -
IndTemp
se incrementa en 1
-
- Lado derecho:
- Se evalúa
newArray1
y se recuerda el resultado. -
IndTemp
se evalúa y el resultado se recuerda (pero aquí hay 1) - El elemento de la matriz se recupera al indexar en la matriz desde el paso 2.1 al índice desde el paso 2.2
- Se evalúa
- De vuelta al lado izquierdo
- El elemento de la matriz se almacena indexando en la matriz del paso 1.1 en el índice del paso 1.2
Como puede ver, la segunda vez que se evalúa IndTemp
(RHS), el valor ya se ha incrementado en 1, pero esto no tiene ningún impacto en el LHS, ya que se recuerda que el valor era 0 antes de aumentar.
En la primera parte del código, el orden es ligeramente diferente:
- Lado izquierdo:
- Se evalúa
newArray2
y se recuerda el resultado. - Se evalúa
IndTemp
y se recuerda el resultado.
- Se evalúa
- Lado derecho:
- Se evalúa
newArray1
y se recuerda el resultado. -
IndTemp
se evalúa y el resultado se recuerda (pero aquí hay 1) -
IndTemp
se incrementa en 1 - El elemento de la matriz se recupera al indexar en la matriz desde el paso 2.1 al índice desde el paso 2.2
- Se evalúa
- De vuelta al lado izquierdo
- El elemento de la matriz se almacena indexando en la matriz del paso 1.1 en el índice del paso 1.2
En este caso, el aumento de la variable en el paso 2.3 no tiene ningún impacto en la iteración del bucle actual, por lo que siempre copiará del índice N
al índice N
, mientras que en la segunda parte del código siempre copiará del índice N+1
en el índice N
.
Eric tiene una entrada de blog titulada Precedence vs order, redux que debe leerse.
Aquí hay un fragmento de código que ilustra, básicamente, convertí las variables en propiedades de una clase e implementé una colección de "arreglos" personalizados, que simplemente vuelcan a la consola lo que está sucediendo.
void Main()
{
Console.WriteLine("first piece of code:");
Context c = new Context();
c.newArray2[c.IndTemp] = c.newArray1[c.IndTemp++];
Console.WriteLine();
Console.WriteLine("second piece of code:");
c = new Context();
c.newArray2[c.IndTemp++] = c.newArray1[c.IndTemp];
}
class Context
{
private Collection _newArray1 = new Collection("newArray1");
private Collection _newArray2 = new Collection("newArray2");
private int _IndTemp;
public Collection newArray1
{
get
{
Console.WriteLine(" reading newArray1");
return _newArray1;
}
}
public Collection newArray2
{
get
{
Console.WriteLine(" reading newArray2");
return _newArray2;
}
}
public int IndTemp
{
get
{
Console.WriteLine(" reading IndTemp (=" + _IndTemp + ")");
return _IndTemp;
}
set
{
Console.WriteLine(" setting IndTemp to " + value);
_IndTemp = value;
}
}
}
class Collection
{
private string _name;
public Collection(string name)
{
_name = name;
}
public int this[int index]
{
get
{
Console.WriteLine(" reading " + _name + "[" + index + "]");
return 0;
}
set
{
Console.WriteLine(" writing " + _name + "[" + index + "]");
}
}
}
La salida es:
first piece of code:
reading newArray2
reading IndTemp (=0)
reading newArray1
reading IndTemp (=0)
setting IndTemp to 1
reading newArray1[0]
writing newArray2[0]
second piece of code:
reading newArray2
reading IndTemp (=0)
setting IndTemp to 1
reading newArray1
reading IndTemp (=1)
reading newArray1[1]
writing newArray2[0]
ILDasm puede ser tu mejor amigo, a veces ;-)
Compilé ambos métodos y comparé el IL resultante (lenguaje ensamblador).
El detalle importante está en el bucle, como era de esperar. Tu primer método compila y se ejecuta así:
Code Description Stack
ldloc.1 Load ref to newArray2 newArray2
ldloc.2 Load value of IndTmp newArray2,0
ldloc.0 Load ref to newArray1 newArray2,0,newArray1
ldloc.2 Load value of IndTmp newArray2,0,newArray1,0
dup Duplicate top of stack newArray2,0,newArray1,0,0
ldc.i4.1 Load 1 newArray2,0,newArray1,0,0,1
add Add top 2 values on stack newArray2,0,newArray1,0,1
stloc.2 Update IndTmp newArray2,0,newArray1,0 <-- IndTmp is 1
ldelem.ref Load array element newArray2,0,"1"
stelem.ref Store array element <empty>
<-- newArray2[0] = "1"
Esto se repite para cada elemento en newArray1. El punto importante es que la ubicación del elemento en la matriz de origen se ha enviado a la pila antes de que se incremente IndTmp.
Compara esto con el segundo método:
Code Description Stack
ldloc.1 Load ref to newArray2 newArray2
ldloc.2 Load value of IndTmp newArray2,0
dup Duplicate top of stack newArray2,0,0
ldc.i4.1 Load 1 newArray2,0,0,1
add Add top 2 values on stack newArray2,0,1
stloc.2 Update IndTmp newArray2,0 <-- IndTmp is 1
ldloc.0 Load ref to newArray1 newArray2,0,newArray1
ldloc.2 Load value of IndTmp newArray2,0,newArray1,1
ldelem.ref Load array element newArray2,0,"2"
stelem.ref Store array element <empty>
<-- newArray2[0] = "2"
Aquí, IndTmp se incrementa antes de que la ubicación del elemento en la matriz de origen se haya insertado en la pila, por lo tanto, la diferencia en el comportamiento (y la posterior excepción).
Para completar, comparémoslo con
newArray2[IndTmp] = newArray1[++IndTmp];
Code Description Stack
ldloc.1 Load ref to newArray2 newArray2
ldloc.2 Load IndTmp newArray2,0
ldloc.0 Load ref to newArray1 newArray2,0,newArray1
ldloc.2 Load IndTmp newArray2,0,newArray1,0
ldc.i4.1 Load 1 newArray2,0,newArray1,0,1
add Add top 2 values on stack newArray2,0,newArray1,1
dup Duplicate top stack entry newArray2,0,newArray1,1,1
stloc.2 Update IndTmp newArray2,0,newArray1,1 <-- IndTmp is 1
ldelem.ref Load array element newArray2,0,"2"
stelem.ref Store array element <empty>
<-- newArray2[0] = "2"
Aquí, el resultado del incremento se ha insertado en la pila (y se convierte en el índice de matriz) antes de que se actualice IndTmp.
En resumen, parece ser que el objetivo de la tarea se evalúa primero, seguido de la fuente .
¡Dale la bienvenida al OP para una pregunta que realmente te hace pensar!
Obviamente, la suposición de que la rhs siempre se evalúa antes de la lhs es incorrecta. Si mira aquí http://msdn.microsoft.com/en-us/library/aa691315(v=VS.71).aspx parece que en el caso de acceso de indexador a los argumentos de la expresión de acceso de indexador, que es el lhs, son evaluados antes de la rs.
en otras palabras, primero se determina dónde almacenar el resultado de la rhs, solo entonces se evalúa la rhs.
newArray1
una excepción porque empiezas a indexar en newArray1
en el índice 1. Ya que estás iterando sobre cada elemento en newArray1
la última asignación lanza una excepción porque IndTmp
es igual a newArray1.Length
, es decir, una pasada el final de la matriz. Incrementa la variable de índice antes de que se use para extraer un elemento de newArray1
, lo que significa que bloqueará y también perderá el primer elemento en newArray1
.
newArray2[IndTmp] = newArray1[IndTmp++];
conduce a primero evaluar y luego incrementar la variable.
- newArray2 [0] = newArray1 [0]
- incremento
- newArray2 [1] = newArray1 [1]
- incremento
y así.
El operador RHS ++ aumenta de inmediato, pero devuelve el valor antes de que se incremente. El valor utilizado para indexar en la matriz es el valor devuelto por el operador RHS ++, por lo tanto, el valor no incrementado.
Lo que describas (la excepción lanzada) será el resultado de un LHS ++:
newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception