php - unir - Concatenación de cadenas mientras se incrementa
concatenar un valor en php (3)
Este es mi código:
$a = 5;
$b = &$a;
echo ++$a.$b++;
¿No debería imprimir 66?
¿Por qué imprime 76?
Bien. En realidad, este es un comportamiento bastante directo, y tiene que ver con cómo funcionan las referencias en PHP. No es un error, sino un comportamiento inesperado.
PHP internamente usa copy-on-write. Lo que significa que las variables internas se copian cuando escribe en ellas (por lo tanto, $a = $b;
no copia la memoria hasta que realmente cambie una de ellas). Con referencias, en realidad nunca se copia. Eso es importante para más tarde.
Miremos esos códigos de operación:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > ASSIGN !0, 5
3 1 ASSIGN_REF !1, !0
4 2 PRE_INC $2 !0
3 POST_INC ~3 !1
4 CONCAT ~4 $2, ~3
5 ECHO ~4
6 > RETURN 1
Los primeros dos deberían ser bastante fáciles de entender.
- ASIGNAR : básicamente, estamos asignando el valor de
5
en la variable compilada llamada!0
. - ASSIGN_REF : estamos creando una referencia de
!0
a!1
(la dirección no importa)
Hasta ahora, eso es sencillo. Ahora viene lo interesante:
- PRE_INC : este es el código de operación que realmente incrementa la variable. Es de destacar que devuelve su resultado en una variable temporal denominada
$2
.
Entonces veamos el código fuente detrás de PRE_INC
cuando se llama con una variable:
static int ZEND_FASTCALL ZEND_PRE_INC_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zend_free_op free_op1;
zval **var_ptr;
SAVE_OPLINE();
var_ptr = _get_zval_ptr_ptr_var(opline->op1.var, execute_data, &free_op1 TSRMLS_CC);
if (IS_VAR == IS_VAR && UNEXPECTED(var_ptr == NULL)) {
zend_error_noreturn(E_ERROR, "Cannot increment/decrement overloaded objects nor string offsets");
}
if (IS_VAR == IS_VAR && UNEXPECTED(*var_ptr == &EG(error_zval))) {
if (RETURN_VALUE_USED(opline)) {
PZVAL_LOCK(&EG(uninitialized_zval));
AI_SET_PTR(&EX_T(opline->result.var), &EG(uninitialized_zval));
}
if (free_op1.var) {zval_ptr_dtor(&free_op1.var);};
CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}
SEPARATE_ZVAL_IF_NOT_REF(var_ptr);
if (UNEXPECTED(Z_TYPE_PP(var_ptr) == IS_OBJECT)
&& Z_OBJ_HANDLER_PP(var_ptr, get)
&& Z_OBJ_HANDLER_PP(var_ptr, set)) {
/* proxy object */
zval *val = Z_OBJ_HANDLER_PP(var_ptr, get)(*var_ptr TSRMLS_CC);
Z_ADDREF_P(val);
fast_increment_function(val);
Z_OBJ_HANDLER_PP(var_ptr, set)(var_ptr, val TSRMLS_CC);
zval_ptr_dtor(&val);
} else {
fast_increment_function(*var_ptr);
}
if (RETURN_VALUE_USED(opline)) {
PZVAL_LOCK(*var_ptr);
AI_SET_PTR(&EX_T(opline->result.var), *var_ptr);
}
if (free_op1.var) {zval_ptr_dtor(&free_op1.var);};
CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}
Ahora no espero que entiendas lo que está haciendo de inmediato (este es el vudú del motor profundo), pero vamos a atravesarlo.
Las primeras dos sentencias if comprueban si la variable es "segura" para incrementar (la primera comprueba si es un objeto sobrecargado, la segunda comprueba si la variable es la variable de error especial $php_error
).
El siguiente es un poco interesante para nosotros. Como estamos modificando el valor, debe preformar copy-on-write. Entonces llama:
SEPARATE_ZVAL_IF_NOT_REF(var_ptr);
Ahora, recuerde, ya configuramos la variable para que sea una referencia más arriba. Entonces, la variable no está separada ... Lo que significa que todo lo que hacemos aquí pasará a $b
también ...
A continuación, la variable se incrementa ( fast_increment_function()
).
Finalmente, establece el resultado como si fuera el mismo . Esto es copiar-escribir nuevamente. No devuelve el valor de la operación, sino la variable real. Entonces, ¿ PRE_INC
devuelve PRE_INC
siendo una referencia a $a
y $b
.
- POST_INC - Esto se comporta de manera similar a
PRE_INC
, excepto por un hecho MUY importante.
Revisemos el código fuente nuevamente:
static int ZEND_FASTCALL ZEND_POST_INC_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
retval = &EX_T(opline->result.var).tmp_var;
ZVAL_COPY_VALUE(retval, *var_ptr);
zendi_zval_copy_ctor(*retval);
SEPARATE_ZVAL_IF_NOT_REF(var_ptr);
fast_increment_function(*var_ptr);
}
Esta vez corté todas las cosas no interesantes. Entonces veamos qué está haciendo.
Primero, obtiene la variable temporal de retorno ( ~3
en nuestro código anterior).
Luego copia el valor de su argumento ( !1
o $b
) en el resultado (y, por lo tanto, la referencia se rompe).
Luego incrementa el argumento.
Ahora recuerde, el argumento !1
es la variable $b
, que tiene una referencia a !0
( $a
) y $2
, que si recuerda fue el resultado de PRE_INC
.
Entonces ahí lo tienes. Devuelve 76 porque la referencia se mantiene en el resultado de PRE_INC.
Podemos probar esto forzando una copia, asignando primero el preinc a una variable temporal (a través de la asignación normal, que romperá la referencia):
$a = 5;
$b = &$a;
$c = ++$a;
$d = $b++;
echo $c.$d;
Que funciona como esperabas Proof
Y podemos reproducir el otro comportamiento (su error) mediante la introducción de una función para mantener la referencia:
function &pre_inc(&$a) {
return ++$a;
}
$a = 5;
$b = &$a;
$c = &pre_inc($a);
$d = $b++;
echo $c.$d;
Que funciona como lo ves (76): Proof
Nota: la única razón para la función separada aquí es que el analizador de PHP no le gusta $c = &++$a;
. Entonces necesitamos agregar un nivel de indirección a través de la llamada de función para hacerlo ...
La razón por la que no considero esto como un error es que así es como se supone que las referencias funcionan. El pre-incremento de una variable referenciada devolverá esa variable. Incluso una variable no referenciada debe devolver esa variable. Puede que no sea lo que esperas aquí, pero funciona bastante bien en casi todos los demás casos ...
El punto subyacente
Si usa referencias, lo está haciendo mal el 99% del tiempo. Así que no uses referencias a menos que las necesites absolutamente . PHP es mucho más inteligente de lo que piensas en optimizaciones de memoria. Y su uso de referencias realmente dificulta la forma en que puede funcionar. Entonces, mientras piensas que puedes estar escribiendo código inteligente, vas a escribir un código menos eficiente y menos amigable la gran mayoría de las veces ...
Y si quiere saber más sobre las referencias y cómo funcionan las variables en PHP, revise uno de mis videos de YouTube sobre el tema ...
Creo que la línea de concatenación completa se ejecuta primero y luego se envía con la función de eco. Por ejemplo
$a = 5;
$b = &$a;
echo ++$a.$b++;
// output 76
$a = 5;
$b = &$a;
echo ++$a;
echo $b++;
// output 66
EDITAR: También muy importante, $ b es igual a 7, pero se repitió antes de agregar:
$a = 5;
$b = &$a;
echo ++$a.$b++; //76
echo $b;
// output 767
EDITAR: agregar el ejemplo de Corbin: https://eval.in/34067
Obviamente hay un error en PHP. Si ejecuta este código:
<?php
{
$a = 5;
echo ++$a.$a++;
}
echo "/n";
{
$a = 5;
$b = &$a;
echo ++$a.$b++;
}
echo "/n";
{
$a = 5;
echo ++$a.$a++;
}
Usted obtiene:
66 76 76
Lo que significa que el mismo bloque (el primero y el tercero son idénticos) del código no siempre devuelve el mismo resultado. Aparentemente la referencia y el incremento están poniendo PHP en un estado falso.