Curso de PHP gratis · Capítulo 11 de 13 · con Iris Espejo
Referencias en PHP: & a fondo y el bug del foreach
Las referencias son uno de los temas más malentendidos de PHP. Aquí verás qué son realmente (dos nombres para una misma caja) y las trampas que causan.
Dos nombres, una caja
Recuerda: cada variable es un nombre en la tabla de símbolos que apunta a una caja (zval). Hasta ahora, cada nombre tenía su propia caja: $b = $a copiaba el valor.
Con & creas una referencia:
$b = &$a;
Esto NO copia nada: hace que el nombre $b apunte a la misma caja que $a. Ahora son dos nombres para un único valor: cambia uno y "cambia" el otro, porque en realidad solo hay una caja.
Por dentro, el motor Zend envuelve el valor en una estructura especial (zend_reference) con un contador: cuántos nombres la comparten (su refcount). En la Lupa verás las cajas compartidas con el mismo color y una etiqueta &A, &B...
Avanza paso a paso y observa cómo $b se une a la caja de $a, mientras que $c tiene su propia copia.
<?php
$a = 1;
$c = $a; // copia el valor
$b = &$a; // comparte la caja
$b = 99;
echo "a=$a b=$b c=$c\n";a=99 b=99 c=1
Una referencia no es un puntero
Si vienes de C, olvida los punteros. Una referencia en PHP no es una dirección de memoria: no puedes sumarle 1 ni imprimirla. Es simplemente otro nombre para la misma caja.
¿Qué hace unset($b)? Borra el nombre, no la caja. La caja sigue viva mientras le quede algún nombre (su refcount sea mayor que 0). Cuando nadie la usa, PHP la libera.
Y si después haces $b = 5, se crea un $b nuevo con su propia caja: el vínculo se ha roto.
Para "desenganchar" una variable de una referencia, unset() es la herramienta correcta. Asignarle otro valor NO la desengancha: ¡cambiaría el valor compartido!
<?php
$a = "luz";
$b = &$a;
unset($b);
echo "a sigue siendo: $a\n";
$b = "sombra";
echo "a=$a b=$b\n";
$c = &$a;
$c = "eco";
echo "a=$a\n";a sigue siendo: luz a=luz b=sombra a=eco
Después de $a = 1; $b = &$a; unset($a);, ¿qué vale $b?
- 1
- null
- Da error
Ver respuesta
A. 1
unset($a) solo borra el nombre $a. La caja sigue viva porque $b apunta a ella, y vale 1.
Paso por referencia
Normalmente una función recibe copias de sus argumentos. Pero si el parámetro lleva &, recibe la caja original:
function subir(&$nivel) { $nivel++; }
Dentro de la función, $nivel y la variable que le pasaste comparten caja: modificar uno es modificar la otra.
Así funcionan sort(), array_push(), shuffle()... Por eso modifican tu array y devuelven true: ¡reciben tu array por referencia!
Solo puedes pasar variables a un parámetro por referencia: subir(5) es un error, porque un 5 no tiene caja que compartir.
<?php
function subir(&$nivel) {
$nivel++;
}
function subir_copia($nivel) {
$nivel++;
}
$x = 1;
subir($x);
subir($x);
subir_copia($x);
echo $x, "\n";3
¿Referencias para ir más rápido? No.
Un mito muy extendido: "pasa los arrays por referencia para no copiarlos y ganar velocidad". Es falso en PHP.
Recuerda el copy-on-write: $b = $a o pasar un array a una función NO lo copia. Ambos apuntan al mismo array y PHP incrementa su refcount. La copia real solo ocurre si alguien escribe mientras está compartido.
De hecho, las referencias pueden hacer tu código más lento: un array que forma parte de una referencia a veces tiene que copiarse cuando no haría falta. Y sobre todo, lo hacen más difícil de entender.
Regla de oro: usa & solo cuando quieras modificar el original a propósito. Si solo quieres leer, pasa por valor.
<?php
function total(array $numeros) {
// solo leemos: PHP no copia el array
return array_sum($numeros);
}
function vaciar(array $numeros) {
// escribimos: se copia (separa) SOLO en este momento
$numeros = [];
return count($numeros);
}
$grande = range(1, 1000);
echo total($grande), "\n";
echo vaciar($grande), " ", count($grande), "\n";500500 0 1000
foreach por referencia
foreach ($array as &$v) hace que $v sea una referencia a cada elemento del array, uno tras otro. Así puedes modificar el array mientras lo recorres:
foreach ($precios as &$p) { $p = $p * 2; } unset($p); // ¡IMPORTANTE!
¿Por qué ese unset($p)? Porque al terminar el bucle, $p sigue siendo una referencia al último elemento. Si más adelante usas $p para otra cosa... cambiarás el último elemento del array sin darte cuenta.
Míralo en la página siguiente: es uno de los bugs más famosos de PHP.
<?php
$precios = [10, 20, 30];
foreach ($precios as &$p) {
$p = $p * 2;
}
print_r($precios);
var_dump($precios);Array
(
[0] => 20
[1] => 40
[2] => 60
)
array(3) {
[0]=>
int(20)
[1]=>
int(40)
[2]=>
&int(60)
}El bug del foreach
Este código parece inofensivo: dobla los números y luego los recorre para mostrarlos. Pero el último elemento sale mal.
Qué pasa, paso a paso (compruébalo con la Lupa):
- Tras el primer bucle,
$ves una referencia a$datos[2](misma caja). - El segundo bucle asigna cada elemento a
$v... ¡pero$vES$datos[2]! - Vuelta 1:
$v = $datos[0]→ escribe 2 en$datos[2]. - Vuelta 2:
$v = $datos[1]→ escribe 4 en$datos[2]. - Vuelta 3:
$v = $datos[2]→ que ahora vale 4.
Resultado: el último elemento es una copia del penúltimo. La solución: unset($v) después del primer bucle (o usar otro nombre de variable).
<?php
$datos = [1, 2, 3];
foreach ($datos as &$v) {
$v = $v * 2;
}
foreach ($datos as $v) {
echo $v, " ";
}
echo "\n";
print_r($datos);2 4 4
Array
(
[0] => 2
[1] => 4
[2] => 4
)¿Qué línea arreglaría el bug del ejemplo?
unset($v);justo después del primer bucle$v = null;después del primer bucle- Poner
&también en el segundo bucle
Ver respuesta
A. unset($v); justo después del primer bucle
unset($v) rompe el vínculo con $datos[2]. En cambio $v = null escribiría null EN el último elemento, ¡porque $v sigue siendo una referencia!
La rareza: referencias dentro de arrays
Ahora algo que casi nadie sabe. Si un elemento de un array forma parte de una referencia, esa casilla sigue siendo compartida aunque copies el array:
$a = [1, 2, 3]; $r = &$a[0]; // la casilla 0 pasa a ser una referencia $b = $a; // "copia"... $b[0] = "X"; // ¡cambia también $a[0]!
¿Por qué? Al copiar un array, PHP copia cada casilla. Una casilla normal se copia por valor, pero una casilla que es una referencia compartida (refcount > 1) se copia... como referencia. Las dos copias del array acaban compartiendo esa caja.
Esto demuestra que las referencias no son "variables especiales": son una propiedad de la caja. Cualquier sitio que apunte a esa caja (una variable, una casilla de array, una propiedad) la comparte.
var_dump marca esas casillas con &. Moraleja: las referencias tienen efectos a distancia. Úsalas con mucho cuidado y deshazlas (unset) cuando termines.
<?php
$a = [1, 2, 3];
$r = &$a[0];
$b = $a;
$b[0] = "X";
echo "a[0] = ", $a[0], "\n";
var_dump($a);
unset($r);
$c = [1, 2];
$c2 = $c;
$c2[0] = "Y";
echo "c[0] = ", $c[0], "\n";a[0] = X
array(3) {
[0]=>
&string(1) "X"
[1]=>
int(2)
[2]=>
int(3)
}
c[0] = 1Objetos: handles, no referencias
Un último espejo. Los objetos (que verás en el Gremio) se comportan de forma parecida a las referencias... pero no son lo mismo.
Una variable con un objeto NO guarda el objeto: guarda un identificador (handle), algo como "el objeto número 1". Al hacer $b = $a se copia el identificador: los dos apuntan al mismo objeto.
- Si la función modifica el objeto recibido (
$o->valor = 5), el cambio se ve fuera: es el mismo objeto. - Si la función reasigna su parámetro (
$o = new ...), fuera no cambia nada: solo cambió su copia del identificador. - Con
&, reasignar SÍ afecta al original: comparten la caja que contiene el identificador.
Mira en la Lupa el montón de objetos: las variables solo guardan #1.
<?php
$dron = new stdClass();
$dron->bateria = 100;
function gastar($d) {
$d->bateria = 50;
$d = new stdClass();
$d->bateria = 0;
}
function cambiar(&$d) {
$d = new stdClass();
$d->bateria = 7;
}
gastar($dron);
echo $dron->bateria, "\n";
cambiar($dron);
echo $dron->bateria, "\n";50 7
Ejercicios de Referencias en PHP resueltos
Intenta resolverlos antes de mirar la solución. En el juego puedes ejecutarlos y comprobarlos automáticamente.
Predicción 1. El reflejo rebelde
Iris duplica precios con un script espejo. Luego los recorre otra vez. ¿Qué queda?
Elige la salida exacta.
<?php
$precios = [10, 20, 30];
foreach ($precios as &$p) {
$p = $p * 2;
}
foreach ($precios as $p) {
}
echo implode(", ", $precios);20, 40, 6020, 40, 4010, 20, 3020, 40, 20
Ver solución
20, 40, 40
El segundo bucle escribió en $precios[2] a través de $p. Míralo paso a paso en la Lupa.
Predicción 2. Nombres y cajas
Tres espejos, tres nombres... ¿cuántas cajas?
Escribe exactamente la salida.
<?php
$a = "sol";
$b = &$a;
$c = $a;
$b = "luna";
unset($a);
echo $b, " ", $c, " ", isset($a) ? "a existe" : "a no existe";Ver solución
luna sol a no existe
$c copió "sol" antes del cambio; unset($a) solo borró un nombre. La caja "luna" sigue viva gracias a $b.
Encargo 3. El intercambio del espejo
Iris necesita un script que intercambie el contenido de dos variables cualesquiera.
Declara intercambiar(&$a, &$b) que intercambie los valores de las dos variables que le pasen. No devuelve nada.
Pistas
- El código de dentro ya es correcto. ¿Qué les falta a los parámetros para modificar las variables de fuera?
function intercambiar(&$a, &$b)
Ver solución
<?php
function intercambiar(&$a, &$b) {
[$a, $b] = [$b, $a];
}
Encargo 4. Subida de precios
La inflación sube 5 monedas cada precio de $precios (un array asociativo que ya existe).
Suma 5 a cada precio de $precios modificando el array con un foreach por referencia. Y como buen mago, rompe la referencia al terminar para que la variable del bucle ya no exista.
Pistas
foreach ($precios as &$p) { $p += 5; }- Después:
unset($p);
Ver solución
<?php
foreach ($precios as &$p) {
$p += 5;
}
unset($p);
Caza de bugs 5. El espejo roto
Este programa pone en mayúscula la inicial de cada nombre y luego los muestra. Pero Eva ha desaparecido y Luis sale dos veces.
Arregla el programa para que imprima Ana Luis Eva (con el espacio final).
Código de partida:
<?php
$nombres = ["ana", "luis", "eva"];
foreach ($nombres as &$n) {
$n = ucfirst($n);
}
foreach ($nombres as $n) {
echo $n, " ";
}
Pistas
- Tras el primer bucle,
$nsigue siendo una referencia a$nombres[2]. - Añade
unset($n);entre los dos bucles.
Ver solución
<?php
$nombres = ["ana", "luis", "eva"];
foreach ($nombres as &$n) {
$n = ucfirst($n);
}
unset($n);
foreach ($nombres as $n) {
echo $n, " ";
}
Misión del dron 6. El contador del espejo
Iris quiere saber cuántas hierbas cosecha BIT-8, llevando la cuenta en una sola variable que varias funciones van actualizando.
Declara cosechar_columna(&$total) que recorra la columna actual (3 casillas hacia el NORTE) cosechando lo maduro y sumando 1 a $total por cada cosecha. Úsala en las 3 columnas del campo y al final imprime Total: 9.
Esta misión mueve al dron BIT-8 por un campo de cultivo: juégala en PHPlay para ver tu código animado.
Pistas
- Sin
&, la función suma en su propia copia de$total. - Llámala tres veces (o en un bucle), moviéndote al ESTE entre columnas.
Ver solución
<?php
function cosechar_columna(&$total) {
for ($i = 0; $i < 3; $i++) {
if (puede_cosechar()) {
cosechar();
$total++;
}
mover(NORTE);
}
}
$total = 0;
for ($col = 0; $col < 3; $col++) {
cosechar_columna($total);
mover(ESTE);
}
echo "Total: $total";
Acertijo 7. El examen de Iris
Iris cruza los brazos: "Última prueba. Sin espejos".
Responde a las cinco preguntas.
¿Qué hace
$b = &$a;?- Copia el valor de
$aen$b - Hace que
$by$acompartan la misma caja - Guarda la dirección de memoria de
$aen$b
Ver respuesta
B. Hace que
$by$acompartan la misma caja- Copia el valor de
Pasar un array grande a una función sin
&...- Copia todo el array siempre (lento)
- No lo copia salvo que la función lo modifique (copy-on-write)
- Da error
Ver respuesta
B. No lo copia salvo que la función lo modifique (copy-on-write)
¿Por qué
sort($a)modifica$a?- Porque recibe el array por referencia
- Porque devuelve el array ordenado
- Porque los arrays son objetos
Ver respuesta
A. Porque recibe el array por referencia
Tras un
foreach ($a as &$v), lo correcto es...$v = null;unset($v);- Nada
Ver respuesta
B.
unset($v);Una función recibe un objeto (sin
&) y hace$obj->x = 5;. Fuera...- No cambia nada
- Se ve el cambio: es el mismo objeto
- Da error
Ver respuesta
B. Se ve el cambio: es el mismo objeto
Chuleta: lo esencial
$b = &$a;: dos nombres, una caja (zend_reference con refcount). Cambiar uno cambia el "otro".- No son punteros: no hay direcciones.
unset($b)borra el nombre; la caja vive mientras tenga nombres. - Asignar a una referencia cambia el valor compartido;
unset()es lo que la desengancha. function f(&$x): la función recibe la caja original. Así funcionansort,array_push... Solo se pueden pasar variables.- Copy-on-write: pasar arrays por valor es barato. No uses
&"por rendimiento". foreach ($a as &$v) { } unset($v);— sin elunset,$vsigue enganchada al último elemento.- Una casilla de array que es referencia compartida sigue compartida al copiar el array.
- Objetos: la variable guarda un handle (#id).
$b = $acomparte el objeto; reasignar un parámetro no afecta fuera (salvo con&).