¿Cuál es la explicación del resultado de la siguiente operación?
k += c += k += c;
Estaba tratando de comprender el resultado de salida del siguiente código:
int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70
y actualmente estoy luchando por entender por qué el resultado de "k" es 80. ¿Por qué la asignación de k = 40 no funciona (en realidad, Visual Studio me dice que ese valor no se está usando en ningún otro lugar)?
¿Por qué k es 80 y no 110?
Si divido la operación en:
k+=c;
c+=k;
k+=c;
el resultado es k = 110.
Estaba tratando de mirar a través del CIL , pero no soy tan profundo en la interpretación del CIL generado y no puedo obtener algunos detalles:
// [11 13 - 11 24]
IL_0001: ldc.i4.s 10
IL_0003: stloc.0 // k
// [12 13 - 12 24]
IL_0004: ldc.i4.s 30
IL_0006: stloc.1 // c
// [13 13 - 13 30]
IL_0007: ldloc.0 // k expect to be 10
IL_0008: ldloc.1 // c
IL_0009: ldloc.0 // k why do we need the second load?
IL_000a: ldloc.1 // c
IL_000b: add // I expect it to be 40
IL_000c: dup // What for?
IL_000d: stloc.0 // k - expected to be 40
IL_000e: add
IL_000f: dup // I presume the "magic" happens here
IL_0010: stloc.1 // c = 70
IL_0011: add
IL_0012: stloc.0 // k = 80??????
c#
cil
compound-assignment
Andrii Kotliarov
fuente
fuente
Respuestas:
Una operación como
a op= b;
es equivalente aa = a op b;
. Una asignación puede usarse como declaración o como expresión, mientras que como expresión da el valor asignado. Tu declaración ...... puede, dado que el operador de asignación es asociativo a la derecha, también puede escribirse como
o (expandido)
k = k + (c = c + (k = k + c)); 10 → 30 → 10 → 30 // operand evaluation order is from left to right | | ↓ ↓ | ↓ 40 ← 10 + 30 // operator evaluation ↓ 70 ← 30 + 40 80 ← 10 + 70
Donde durante toda la evaluación se utilizan los valores antiguos de las variables involucradas. Esto es especialmente cierto por el valor de
k
(vea mi revisión del IL a continuación y el enlace que proporcionó Wai Ha Lee). Por lo tanto, no obtiene 70 + 40 (nuevo valor dek
) = 110, sino 70 + 10 (antiguo valor dek
) = 80.El punto es que (de acuerdo con la especificación de C # ) "los operandos en una expresión se evalúan de izquierda a derecha" (los operandos son las variables
c
yk
en nuestro caso). Esto es independiente de la precedencia del operador y la asociatividad que en este caso dictan un orden de ejecución de derecha a izquierda. (Vea los comentarios a la respuesta de Eric Lippert en esta página).Ahora miremos el IL. IL asume una máquina virtual basada en pila, es decir, no usa registros.
IL_0007: ldloc.0 // k (is 10) IL_0008: ldloc.1 // c (is 30) IL_0009: ldloc.0 // k (is 10) IL_000a: ldloc.1 // c (is 30)
La pila ahora se ve así (de izquierda a derecha; la parte superior de la pila está a la derecha)
IL_000b: add // pops the 2 top (right) positions, adds them and pushes the sum back
IL_000d: stloc.0 // k <-- 40
IL_000e: add
IL_0010: stloc.1 // c <-- 70
IL_0011: add
IL_0012: stloc.0 // k <-- 80
Tenga en cuenta que
IL_000c: dup
,IL_000d: stloc.0
es decir, la primera asignación ak
, podría optimizarse. Probablemente esto se haga para las variables por el jitter al convertir IL a código de máquina.Tenga en cuenta también que todos los valores requeridos por el cálculo se envían a la pila antes de realizar cualquier asignación o se calculan a partir de estos valores. Valores asignados (por
stloc
) nunca se reutilizan durante esta evaluación.stloc
aparece la parte superior de la pila.El resultado de la siguiente prueba de consola es (
Release
modo con optimizaciones activadas)private static int _k = 10; public static int k { get { Console.WriteLine($"evaluating k ({_k})"); return _k; } set { Console.WriteLine($"{value} assigned to k"); _k = value; } } private static int _c = 30; public static int c { get { Console.WriteLine($"evaluating c ({_c})"); return _c; } set { Console.WriteLine($"{value} assigned to c"); _c = value; } } public static void Test() { k += c += k += c; }
fuente
k = 10 + (30 + (10 + 30)) = 80
y esec
valor final se establece en el primer paréntesis que esc = 30 + (10 + 30) = 70
.k
es un local, es casi seguro que la tienda muerta se elimina si las optimizaciones están activadas y se conserva si no lo están. Una pregunta interesante es si se permite que el jitter elide el almacén muerto sik
es un campo, propiedad, ranura de matriz, etc. en la práctica, creo que no.k
se asigna dos veces si es una propiedad.En primer lugar, las respuestas de Henk y Olivier son correctas; Quiero explicarlo de una manera ligeramente diferente. Específicamente, quiero abordar este punto que hizo. Tienes este conjunto de declaraciones:
int k = 10; int c = 30; k += c += k += c;
Y luego concluye incorrectamente que esto debería dar el mismo resultado que este conjunto de declaraciones:
int k = 10; int c = 30; k += c; c += k; k += c;
Es informativo ver cómo se equivocó y cómo hacerlo bien. La forma correcta de descomponerlo es así.
Primero, reescribe el más externo + =
En segundo lugar, reescribe el + más externo. Espero que esté de acuerdo en que x = y + z siempre debe ser lo mismo que "evaluar y como temporal, evaluar z como temporal, sumar los temporales, asignar la suma ax" . Así que hagámoslo muy explícito:
int t1 = k; int t2 = (c += k += c); k = t1 + t2;
Asegúrese de que esté claro, porque este es el paso que se equivocó . Al dividir operaciones complejas en operaciones más simples, debe asegurarse de hacerlo lenta y cuidadosamente y de no omitir pasos . Saltar pasos es donde cometemos errores.
Bien, ahora divida la asignación en t2, nuevamente, lenta y cuidadosamente.
int t1 = k; int t2 = (c = c + (k += c)); k = t1 + t2;
La asignación asignará el mismo valor a t2 que a c, así que digamos que:
int t1 = k; int t2 = c + (k += c); c = t2; k = t1 + t2;
Excelente. Ahora analice la segunda línea:
int t1 = k; int t3 = c; int t4 = (k += c); int t2 = t3 + t4; c = t2; k = t1 + t2;
Genial, estamos progresando. Divida la asignación en t4:
int t1 = k; int t3 = c; int t4 = (k = k + c); int t2 = t3 + t4; c = t2; k = t1 + t2;
Ahora analice la tercera línea:
int t1 = k; int t3 = c; int t4 = k + c; k = t4; int t2 = t3 + t4; c = t2; k = t1 + t2;
Y ahora podemos ver todo:
int k = 10; // 10 int c = 30; // 30 int t1 = k; // 10 int t3 = c; // 30 int t4 = k + c; // 40 k = t4; // 40 int t2 = t3 + t4; // 70 c = t2; // 70 k = t1 + t2; // 80
Entonces, cuando terminamos, k es 80 y c es 70.
Ahora veamos cómo se implementa esto en IL:
int t1 = k; int t3 = c; is implemented as ldloc.0 // stack slot 1 is t1 ldloc.1 // stack slot 2 is t3
Ahora bien, esto es un poco complicado:
int t4 = k + c; k = t4; is implemented as ldloc.0 // load k ldloc.1 // load c add // sum them to stack slot 3 dup // t4 is stack slot 3, and is now equal to the sum stloc.0 // k is now also equal to the sum
Podríamos haber implementado lo anterior como
ldloc.0 // load k ldloc.1 // load c add // sum them stloc.0 // k is now equal to the sum ldloc.0 // t4 is now equal to k
pero usamos el truco "dup" porque acorta el código y facilita el jitter, y obtenemos el mismo resultado. En general, el generador de código C # intenta mantener los temporales "efímeros" en la pila tanto como sea posible. Si le resulta más fácil seguir la IL con menos efímeras, a su vez optimizaciones fuera , y el generador de código será menos agresivo.
Ahora tenemos que hacer el mismo truco para obtener c:
int t2 = t3 + t4; // 70 c = t2; // 70 is implemented as: add // t3 and t4 are the top of the stack. dup stloc.1 // again, we do the dup trick to get the sum in // both c and t2, which is stack slot 2.
y finalmente:
k = t1 + t2; is implemented as add // stack slots 1 and 2 are t1 and t2. stloc.0 // Store the sum to k.
Como no necesitamos la suma para nada más, no la duplicamos. La pila ahora está vacía y estamos al final de la declaración.
La moraleja de la historia es: cuando intente comprender un programa complicado, siempre analice las operaciones una por una . No tome atajos; te llevarán por mal camino.
fuente
F(i) + G(i++) * H(i)
, se llama al método F usando el valor anterior de i, luego al método G se llama con el antiguo valor de i y, finalmente, el método H se llama con el nuevo valor de i . Esto es independiente y no está relacionado con la precedencia del operador ". (Énfasis agregado.) ¡Así que supongo que estaba equivocado cuando dije que no hay ningún lugar donde ocurra "el valor anterior"! Ocurre en un ejemplo. Pero el bit normativo es "de izquierda a derecha".+
, y luego lo obtendrá+=
gratis porquex += y
se define comox = x + y
excepto quex
se evalúa solo una vez. Eso es cierto independientemente de si+
está integrado o definido por el usuario. Entonces: intente sobrecargar+
un tipo de referencia y vea qué sucede.Se reduce a: ¿se
+=
aplica lo primero al originalk
o al valor que se calculó más a la derecha?La respuesta es que, aunque las asignaciones se vinculan de derecha a izquierda, las operaciones siguen avanzando de izquierda a derecha.
Entonces el de la izquierda
+=
está ejecutando10 += 70
.fuente
Probé el ejemplo con gcc y pgcc y obtuve 110. Verifiqué el IR que generaron y el compilador expandió el expr a:
k = 10; c = 30; k = c+k; c = c+k; k = c+k;
que me parece razonable.
fuente
para este tipo de asignaciones en cadena, debe asignar los valores comenzando en el lado más a la derecha. Tienes que asignarlo y calcularlo y asignarlo al lado izquierdo, y continuar con esto hasta la final (asignación más a la izquierda). Seguro que se calcula como k = 80.
fuente
Respuesta simple: Reemplaza vars con valores y lo tienes:
int k = 10; int c = 30; k += c += k += c; 10 += 30 += 10 += 30 = 10 + 30 + 10 + 30 = 80 !!!
fuente
k = 10; m = (k += k) + k;
no significam = (10 + 10) + 10
. Los lenguajes con expresiones mutantes no pueden analizarse como si tuvieran una ansiosa sustitución de valor . La sustitución de valores ocurre en un orden particular con respecto a las mutaciones y debes tener eso en cuenta.Puedes resolver esto contando.
Hay dos
c
sy dosk
s así quea = 2c + 2k
Y, como consecuencia de los operadores del lenguaje,
k
también es igual a2c + 2k
Esto funcionará para cualquier combinación de variables en este estilo de cadena:
Entonces
a = 2m + n + 3r
Y
r
será igual a lo mismo.Puede calcular los valores de los otros números calculando solo hasta su asignación más a la izquierda. Entonces
m
es igual2m + n
yn
es igualn + m
.Esto demuestra que
k += c += k += c;
es diferentek += c; c += k; k += c;
y, por lo tanto, obtiene respuestas diferentes.Algunas personas en los comentarios parecen estar preocupadas de que pueda intentar generalizar en exceso desde este atajo a todos los tipos posibles de adición. Entonces, dejaré en claro que este atajo solo es aplicable a esta situación, es decir, encadenar asignaciones de suma para los tipos de números integrados. No funciona (necesariamente) si agrega otros operadores en, por ejemplo,
()
o+
, o si llama a funciones o si ha anulado+=
, o si está usando algo diferente a los tipos de números básicos. Solo está destinado a ayudar con la situación particular en la pregunta .fuente
x = 1;
yy = (x += x) + x;
¿es su opinión que "hay tres x y, por lo tanto, y es igual a3 * x
"? Porquey
es igual a4
en este caso. Ahora bien, ¿qué pasay = x + (x += x);
con su opinión de que la ley algebraica "a + b = b + a" se cumple y esto también es 4? Porque esto es 3. Desafortunadamente, C # no sigue las reglas del álgebra de la escuela secundaria si hay efectos secundarios en las expresiones . C # sigue las reglas de un álgebra de efectos secundarios.