Las referencias en PHP: qué son y el porqué de no usarlas

elefante
elefante

Según un estudio reciente, PHP es actualmente el tercer lenguaje de programación más popular.  Puede sorprender teniendo en cuenta los buenos comentarios que han recibido durante años lenguajes como Ruby y que se daba por seguro el retroceso de PHP. Yo creo que esto es debido a que la gente que está detrás de PHP ha sabido adaptarse a las nuevas tendencias y responder a tiempo. Dos ejemplos muy significativos:

  • PHP5
  • Zend Framework (y otros)

Sorry this post is only in Spanish :
https://ivanmosquera.files.wordpress.com/2010/02/elephant-php.pngindex.php/2010/02/13/las-referencia…que-no-usarlas/?lang=es
La comunidad “phpera” lejos de quedarse parada ante el adelantamiento por la derecha que parecía que les iba a hacer Ruby/Python/etc han mejorado el lenguaje increíblemente hasta el punto que el diseño OO de PHP tiene poco que envidiar a Java. Me imagino a los desarrolladores allá por el 2003 diciéndo “Dicen que PHP4 no es enterprise-ready, que su OO da asco, vale, tienen razón.”. Y de ahí salió PHP5, miras la lista de cambios entre PHP4 y PHP5, y resulta increíble que sólo les separe un número de versión. Algo parecido se puede decir de “Zend Framework”: “Ruby on Rails mola mucho, un killer app, hagamos algo parecido”. Y acabó surgiendo Zend Framework, un excelente framework MVC con “total” cobertura de test unitarios y completamente modular. Además, surgieron muchos otros framework que rivalizan con ZF, el “oficial”, creando una competencia absolutamente sana. Se trata de ejemplos como CakePHP, Symfony o CodeIgniter. El último episodio donde se ha demostrado este pragmatismo de los phperos ha sido dejar Zend_Entity y pasar a adoptar el ORM supuestamente de la competencia : Doctrine. Esto contrasta con la gente de Perl que ha dedicado la mayor parte de sus esfuerzos a crear una máquina virtual, académicamente muy interesante pero en la práctica ¿Hay nuevos usuarios de Perl existiendo PHP, Python y Ruby?.

Vale, a estas alturas te estarás preguntando a qué viene el título de esta entrada. Pues bien, todo esto de la actitud pragmática, los grandes avances etc. está muy bien pero existe un problema en PHP si bien en mi opinión es un mal menor: se trata de mantener la compatibilidad hacia atrás. No sólo un mal menor sino además un mal necesario que permite que los desarrolladores vayan adaptándose a los nuevos cambios progresivamente. La opción contraria podría resultar crítica para muchos usuarios, hay pocos casos donde ha sido necesario ese caso extremo, me viene a la cabeza VisualBasic 6 y poco más.

PHP es un lenguaje que empezó con objetivos mucho más modestos, seguramente Rasmus Lerdof ni en sus sueños más húmedos se imaginó que su herramienta evolucionaría hasta soportar sitios de la escala de Facebook. La política en PHP ha sido ir corrigiendo cosas y manteniendo compatibilidad hacia atrás avisando al desarrollador de las malas prácticas mediante warnings. En esta entrada hablaré de porqué no se debe utilizar a día de hoy referencias en PHP.

¿Qué son las referencias? “&”

Las referencias de PHP se parecen en esencia a las referencias (que no punteros) de C++. Son algo parecido a los enlaces duros de UNIX. Se han utilizado mucho en PHP debido a que en PHP4 el diseño OO era bastante distinto. En POO se trabaja con referencias de objetos en lugar de con los datos directamente debido a que la idea es mandar mensajes a los objetos y que ellos mismos sean los encargados de modificar su propio estado, y para eso basta la referencia del objeto para mandarle mensajes, difícilmente vas a necesitar acceder al objeto en sí mismo.
En PHP4, la implementación OO hacía que fuera necesario utilizar el operador referencia para hacer POO ya que de lo contrario estabas copiando objetos. También hay que tener en cuenta que hasta hace relativamente poco no se programaba OO en PHP por lo que se asumía que al igual que en C, si quieres trabajar con una estructura de datos muy grande, lo suyo es pasarlo por referencia y así evitar la copia. Esto me lleva a explicar un hecho poco conocido de PHP.

copy-on-write

La práctica de usar paso por referencia por motivos de optimización es equivocada actualmente debido a que PHP hace copy-on-write. Esto consiste en que PHP cuando se pasa por valor (casi siempre), no hace una copia de la variable desde el principio sino que trabajará en realidad con la referencia y hará una copia sólo en el momento en que detecte que vas a intentar modificar. Es decir, tú como programador feliz, estás en el ámbito de la función y con un parámetro por valor, y parece que estás trabajando desde el principio con una copia con lo cual si la estructura de datos es muy grande te puedes ver tentado a pasar por referencia pero es un error debido a que PHP internamente no hace la copia hasta que sea realmente necesario. Vale, pero puedes estar pensando: ¡ pero es que precisamente mi función va a modificar esa estructura y mucho así que se va a hacer esa costosa copia! Bien, el problema subyacente es que no se está pensando suficientemente a lo OO sino más bien proceduralmente. En PHP5 el modelo de objetos está rediseñado de modo que los objetos se trabajan con referencias como en Java, por lo que deja de ser necesario utilizar el operador referencia para hacer POO. Esto supone que el operador ‘=’ en la práctica es como si copiara sólo para los tipos primitivos mientras que para los objetos copia la referencia. Esta suele ser la típica explicación pero en realidad es errónea, lo cierto es que esta misma mala explicación también se suele dar en Java como simplificación. Lo que ocurre en realidad es que en PHP5 no trabajas con el objeto directamente, trabajas con su referencia, de modo que cuando pasas el objeto por valor, como mucho estarás provocando (copy-on-write) la copia de la referencia pero no del objeto. Si quieres una copia física del objeto lo que necesitarás es clonar el objeto (clone). Puedes utilizar el operador de referencia con objetos también pero resulta innecesario.

Vamos a ver más en profundidad cómo trabaja el intérprete de PHP las variables.

apt-get source php5-cli.

La estructura de datos que almacena una variable en PHP se llama “zval_struct” (zval entre amigos). Un “zval” tiene los siguientes campos:

  • tipo (el tipo de la variable)
  • valor (el valor de la variable)
  • is_ref (flag que indica si la variable es una referencia &)
  • refcount (contador del número de símbolos que apuntan a este zval)

La estructura está definida en zend.h:

struct _zval_struct {
/* Variable information */
zvalue_value value;             /* value */
zend_uint refcount;
zend_uchar type;        /* active type */
zend_uchar is_ref;
};

Un zvalue_value puede ser muchas cosas. Ahí reside la magia del tipado de php. Tenemos en la misma estructura de datos las distintas posibilidades, tanto tipos primitivos como objetos, en Ikuspro realicé una implementación similar sólo que la genericidad de Java facilita mucho las cosas. Los objetos no se almacenan en esta estructura sino que únicamente se guarda el puntero (en PHP-5).

typedef union _zvalue_value {
        long lval;                                      /* long value */
        double dval;                            /* double value */
        struct {
                char *val;
                int len;
        } str;
        HashTable *ht;                          /* hash table value */
        zend_object_value obj;
} zvalue_value;

En PHP4 probablemente el diseño era distinto de manera que en el valor iba directamente el objeto de manera que era necesario usar continuamente el operador referencia para hacer programación orientada a objetos correctamente.

Un zend_object_value es lo siguiente (zend_types.h):

typedef struct _zend_object_value {
        zend_object_handle handle;
        zend_object_handlers *handlers;
} zend_object_value;

El handle es un ID único de entre ese tipo concreto de objetos (no global). El tipo del objeto y su funcionalidad está en esta otra estructura que es encuentra en una tabla. Desde zend_object_value vemos que se apunta con handlers en la entrada adecuada.

typedef struct _zend_object_handlers {
        zend_object_add_ref_t                    add_ref;
        zend_object_del_ref_t                    del_ref;
        zend_object_delete_obj_t                 delete_obj;
        zend_object_clone_obj_t                  clone_obj;
        zend_object_read_property_t              read_property;
        zend_object_write_property_              write_property;
        zend_object_get_property_ptr_t           get_property_ptr;
        zend_object_get_property_zval_ptr_t      get_property_zval_ptr;
        zend_object_get_t                        get;
        zend_object_set_t                        set;
        zend_object_has_property_t               has_property;
        zend_object_unset_property_t             unset_property;
        zend_object_get_properties_t             get_properties;
        zend_object_get_method_t                 get_method;
        zend_object_call_method_t                call_method;
        zend_object_get_constructor_t            get_constructor;
        zend_object_get_class_entry_t            get_class_entry;
        zend_object_get_class_name_t             get_class_name;
        zend_object_compare_t                    compare_objects;
} zend_object_handlers;

Los símbolos (nombres de variables) que apuntan a un zval se guardan en una tabla de símbolos. Normalmente solemos hablar de variables locales, variables globales, ámbitos de variables… Bien, de lo que se trata es que tenemos una tabla de símbolos por ámbito y en PHP el ámbito está unido a las funciones o métodos. En C por ejemplo, es diferente y los ámbitos van unidos a los bloques directamente de modo que tenemos tablas de símbolos por bloques. Por ejemplo en PHP tenemos:

$a = "hola/";
echo $a;
{
          $a = "jaja/";
          echo $a;
}
echo $a;

El resultado será “hola/jaja/jaja/”

En C:

#include
main() {

char *a = "hola/";
printf("%s",a);

{
char *a = "jaja/";
printf("%s",a);
}

printf("%s",a);

}

El resultado es “hola/jaja/hola/”.

Si te parece un detalle de importancia echa un vistazo a este código:

<?
$a = "Hola";
echo $a;

if ($a == "Hola") {
        $b = "Adios";
}
echo $b;

Este código en C no tiene sentido.

Podemos ver los zval con xdebug_debug_zval

apt-get install php5-xdebug

Veamos un ejemplo:

atrib1 = "foo";
$obj->atrib2 = "bar";
xdebug_debug_zval('obj');
$obj2 = $obj;
xdebug_debug_zval('obj2');
$obj3 = &$obj;
xdebug_debug_zval('obj3');

El resultado es:

a: (refcount=1, is_ref=0)='Hola'
b: (refcount=1, is_ref=0)=2
obj: (refcount=1, is_ref=0)=class stdClass { public $atrib1 = (refcount=1, is_ref=0)='foo'; public $atrib2 = (refcount=1, is_ref=0)='bar' }
obj2: (refcount=2, is_ref=0)=class stdClass { public $atrib1 = (refcount=1, is_ref=0)='foo'; public $atrib2 = (refcount=1, is_ref=0)='bar' }
obj3: (refcount=2, is_ref=1)=class stdClass { public $atrib1 = (refcount=1, is_ref=0)='foo'; public $atrib2 = (refcount=1, is_ref=0)='bar' }

Como puedes ver el único con is_ref=1 es la variable referencia. Un zval se elmina cuando refcount pasa a ser cero y esas referencias van desaparenciendo a medida que se van resolviendo los ámbitos. Es decir, si hemos creado un zval dentro de una función (localmente) tendremos en principio símbolos pertenecientes a la tabla de símbolos de esa función. Al terminar la función ese zval pasará a tener 0 de refcount y por tanto se podrá liberar. Se suele decir que la función unset() de php destruye objetos pero creo que eso no es cierto. Un ejempo:

$orig = 4;

$a = $orig;
xdebug_debug_zval('a');
$b = &$a;
xdebug_debug_zval('a');
unset($a);
xdebug_debug_zval('b');
echo $b ."\n";
$obj = new stdClass;
$obj->foo = "bar";
$obj2 = &$obj;
xdebug_debug_zval('obj');
unset($obj);
xdebug_debug_zval('obj2');
echo $obj2->foo ."\n";

El resultado es:

a: (refcount=2, is_ref=0)=4
a: (refcount=2, is_ref=1)=4
b: (refcount=1, is_ref=0)=4
4
obj: (refcount=2, is_ref=1)=class stdClass { public $foo = (refcount=1, is_ref=0)='bar' }
obj2: (refcount=1, is_ref=0)=class stdClass { public $foo = (refcount=1, is_ref=0)='bar' }
bar

Como podemos ver lo que hace unset en realidad es disminuir el número del contador. Por último un ejemplo en el que pasamos la referencia de un objeto con lo cual hacemos que deje de apuntar a un objeto y pase a ser un string (tipo primitivo):

foo = "bar";
xdebug_debug_zval('a');
lafuncion(&$a);
xdebug_debug_zval('a');

El resultado es:

a: (refcount=1, is_ref=0)=class stdClass { public $foo = (refcount=1, is_ref=0)='bar' }
a: (refcount=1, is_ref=0)='Sorpresa'

Como conclusión: no merece la pena trabajar con el operador referencia si estamos haciendo POO, lo único que vamos a conseguir es liar el código y provocar bugs, los punteros y referencias no están al nivel de abstracción que solemos manejar en nuestra vida diaria mientras que los objetos, atributos y métodos sí. Desgraciadamente existe mucho código “legacy” que hace uso intensivo de pasos por referencia, incluyendo algunas funciones de ordenación de PHP. Una opción para convertir esas funciones es sustituir los parámetros por referencia por un objeto stdClass que los encapsule pasado por valor.

Links:

Para escribir este post me he basado en el código citado del intérprete de PHP y en los siguientes enlaces. Si crees que he cometido algún error pon un comentario, estaré encantado en aprender:

http://php.net/manual/en/features.gc.refcounting-basics.php
http://schlueters.de/blog/archives/125-Do-not-use-PHP-references.html
http://blog.libssh2.org/index.php?/archives/51-Youre-being-lied-to..html

Advertisement

Published by

Iván Mosquera Paulo

Software Engineer

4 thoughts on “Las referencias en PHP: qué son y el porqué de no usarlas”

  1. Muy bueno e interesante el post, enhorabuena!

    Sobre la compatibilidad hacia atrás, creo que a veces es necesaria, para no arrastrar demasiada ‘mierda’ con el transcurso del tiempo. Por ejemplo python 3 lo ha hecho, y aunque es una putada para los developers, es bueno para el lenguaje: )

  2. Muchas gracias a ver si le cojo el tranquillo a esto de bloguear XDDDD

    Lo cierto es que Python me cae bien, no así Ruby por culpa de que me recuerda Perl y bueno… dejé la época de “Uuu! mira todo lo que hago en una línea”. No digo que Ruby sea malo, seguro que está muy bien pero ahora mismo valoro más el código fácil de leer.
    Y bueno, python debe ser mejor en eso pero Python al final es una curva de aprendizaje más y es tiempo que quitar para otras batallas. Si lo llevamos a nivel de empresa mejor no calcular el dinero que supondría dejar PHP para pasar a Python porque lo mismo los fríos números dicen que saldría mejor cambiar de plantilla 😦 .

    Lo que sí he visto es empresas que han pasado de lenguajes pesados (Java) a Ruby/Python, supongo que tanta capa era absurdos en algunos casos y esa curva se compensa de sobra.

    Por eso creo que la compatibilidad hacia atrás es un mal menor en el caso de PHP, ya que se van añadiendo las funcionalidades y la gente las va asumiendo al ritmo que considere.

    De hecho este post ha sido motivado por un nuevo warning que saca PHP 5.3:
    Warning: Call-time pass-by-reference has been deprecated
    (que creo que en realidad no implica que no puedas usar referencias sino que no uses el operador de referencia en las llamadas a función).

    Así que poco a poco a base de warnings se van cambiando las prácticas.

    Lo dicho, gracias por leer y por comentar 🙂

  3. Este post necesita una aclaración: lamentablemente en PHP hay situaciones en que sigue siendo necesario trabajar con referencias. El mensaje viene a ser que se eviten siempre que sea posible. Si por ejemplo estás utilizando referencias por eficiencia, puedes encapsular esa estructura en un objeto para evitarlo.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s