12 de marzo de 2011

[PHP] Arrays pasados por parámetro ¿copias o referencias? (1/2)

conjuntoEntradas() {
  [PHP] Arrays pasados por parámetro ¿copias o referencias? (1/2)
  [PHP] Arrays pasados por parámetro ¿copias o referencias? (2/2)
}


Introducción

Realizando mis jugueteos con la programación en PHP, una de las cosas que necesitaba hacer era pasar arrays a objetos de clases creadas por mi mismo; en algunas de estas clases he considerado oportuno mantener los valores que se pasan por parámetro a través de un array en uno de los atributos de la clase debido a que me pueden ser necesarios para realizar operaciones futuras con o sobre esos valores.

Hasta aquí nada nuevo, ya que es una práctica muy utilizada; pero a mí, que no soy, para nada, un “ninja” del PHP, me hizo pensar que realmente sucedía al pasar un array por parámetro de las distintas maneras que permite el lenguaje, valor o referencia y la manera de asignarlo a un atributo, valor o referencia. Por mis conocimientos, sabía los distintos comportamientos que podían suceder pero, al no estar seguro de cual sería el que se aplicaría en PHP, y si teniendo lo muy claro en otros lenguajes donde tu puedes controlar las posiciones de memoria con referencias y, con mayor control, con punteros, como es el caso de C y su hermano C++.

Como después de buscar y revisar un poco la documentación y algunos enlaces de cuatro búsquedas por Internet no me continuaba sin quedar del todo claro, decidí hacer un script PHP muy simple que me permitiera observar el comportamiento del lenguaje antes esta común situación.
La pequeña prueba
El script que cree, hace lo siguiente:
  1. Declara una clase con:
    • Tres atributos; uno para almacenar el parámetro que se pasará a a cada uno de los tres métodos declarados.
    • Tres métodos, cada uno recibe un array por parámetro, pero uno lo recibe por valor y los otros dos por referencia; la diferencia entre los dos métodos que reciben el array por referencia está en su interior, además de que cada uno asigna el array a un atributo distinto, uno lo asigna el valor y otro asigna la referencia.
      Cada método, además de asignar el array a un atributo, realiza una modificación (añade un nuevo valor) en el array pasado por parámetro.
    class A {
        private $arrValor;
        private $arrValorToRef;
        private $arrReferencia;
        private $arrRefToRef;
        
    
        public function setArrayByValue(array $setArr) {
            $this->arrValor = $setArr;
            $setArr['keyMarkClassAValue'] = 'valueMarkClassA';
            $this->arrValor['keyClassA'] = 'valueClassA';
        }
    
        public function setArrayByValueToRef(array $setArr) {
            $this->arrValorToRef = &$setArr;
            $setArr['keyMarkClassAValueToRef'] = 'valueMarkClassA';
            $this->arrValor['keyClassA'] = 'valueToRefClassA';
        }
    
        public function setArrayByRef(array &$setArr) {
            $this->arrReferencia = $setArr;
            $setArr['keyMarkClassARef'] = 'refMarkClassA';
            $this->arrReferencia['keyClassA'] = 'refClassA';
        }
    
        public function setArrayByRefToRef(array &$setArr) {
            $this->arrRefToRef = &$setArr;
            $setArr['keyMarkClassARefToRef'] = 'refToRefMarkClassA';
            $this->arrRefToRef['keyClassA'] = 'refToRefClassA';
        }
    
    }
    
  2. Declara una array con un valor en su interior, el que se utilizará para pasarlo a los tres métodos de un objeto, de la clase declarada, creado.
    $arr = array('key0' => 'value0');
    
  3. Se van realizando dumpeos del array utilizado como parámetro y del objeto a medida que se van ejecutando los métodos para observar comportamiento.
    print "Contenido del array \$arr antes de asignarlo a la clase: \n";
    var_dump($arr);
    print "\n";
    
    $objA = new A();
    $objA->setArrayByValue($arr);
    
    print "Contenido del array \$arr despues de asignarlo a la clase por valor: \n";
    var_dump($arr);
    print "\n";
    
    $objA->setArrayByRef($arr);
    
    print "Contenido del array \$arr despues de asignarlo a la clase por referencia: \n";
    var_dump($arr);
    print "\n";
    
    $objA->setArrayByValueToRef($arr);
    $objA->setArrayByRefToRef($arr);
    
    print "Contenido del object de A despues de asignarle el array (valor y referencias): \n";
    var_dump($objA);
    print "\n";
    
    $arr['newKey'] = 'newValue';
    
    print "Contenido del object de A despues de ejecutar \$arr['newKey'] => 'newValue': \n";
    var_dump($objA);
    print "\n";
    
    unset($arr);
    
    print "Contenido del object de A despues de ejecutar un unset() del array: \n";
    var_dump($objA);
    print "\n";
    

El encapsulamiento

Con la salida que genera el script podemos llegar a ciertas conclusiones sobre lo que sucede con la asignación del array pasado por parámetro a los atributos del objeto, con las distintas maneras que se permiten, y ver si realmente el atributo del objeto mantiene una referencia a esa posición de memoria o copia el valor, desde el punto de vista de utilización habitual del lenguaje, es decir sin buscar saltarnos lo establecido, ya que al referirme que se puede estar copiando el valor del array a otra posición de memoria, me refiero a la percepción que se tiene como desarrollador, dejando de lado el funcionamiento interno del interprete, ya que este podría estar optimizando el rendimiento de la ejecución demorando la copia de los valores a una nueva posición de memoria hasta el momento antes de que alguno de los valores de la variable sufra una modificación, garantizando e esta manera la integridad de los valores de cada variable.

El resultado obtenido es el siguiente:
  1. Parámetro por valor y asignación por valor. Variable pasada por parámetro y atributo utilizan su propio espacio de memoria.
  2. Parámetro por valor y asignación por referencia. Variable pasada por parámetro es copiada a un nuevo espacio de memoria ya que cuando se asigna por referencia al atributo (su dirección de memoria), los cambios en los datos de cada variable son independientes.
  3. Parámetro por referencia y asignación por valor. Variable pasada por parámetro, supuestamente no es copiada, ya que se está pasando por referencia, pero al asignarla al atributo es cuando se realiza una copia de los datos de esta variable a nuevo espacio de memoria que será donde apuntará el atributo al que se está asignando.
  4. Parámetro pasado por referencia y asignación por referencia. No hay copias de los datos a nuevos espacios de memoria por lo que, tanto la variable pasada por parámetro como el atributo apuntan al mismo espacio de memoria.

Con las cuatro pruebas anteriores se puede deducir que las referencias en PHP son tratadas como en otro lenguaje, como C/C++ (donde el control de la memoria depende totalmente del desarrollador), pero con algo menos de liberta, ya que el lenguaje controla el número de variables que apuntan al mismo espacio de memoria y bajo la existencia de una sola, se imposibilita su liberación impidiendo que una variable haga referencia a un espacio de memoria destruido (caso b), además de también imposibilitar la liberación solicitada explícitamente (unset).

Esto ya me permitía saber que sucedía, en PHP, con los arrays, cuando son utilizados como parámetro en una función/método y en las asignaciones a nuevas variables, pudiendo decidir como hacerlo, si por valor o por referencia, en función de lo que realmente quiero hacer con ellos.
Este tipo de dudas siempre se me plantean con los lenguajes, donde el control de la memoria se hace automáticamente mediante un garbage collector, ya que en algunos, como Java, que aunque no existe el concepto de referencia y valor, si que existen los immutable objects en el mismo JDK, provocando un comportamiento distinto, en objetos de ciertas clases, delante de estos casos.

Hasta la próxima enfermos.

1 comentario:

  1. Buscaba hacer lo mismo pasar un array con muchos datos referenciado, como lo hago en perl.

    ResponderEliminar