Para cada uno de mis descuidos…

Un problema rápido en PHP, un lenguaje que todo el mundo dice conocer… y del que en realidad muy pocos tienen algo de idea:

<?php

class Fruit
{
   function __construct($name)
   {
      $this->name=$name;
      $this->stock['current']=array('value' => 3);
      $this->stock['cache']=array('value' => 3);
   }

   function setStock($new_stock)
   {
      foreach($this->stock as $key => $current_stock)
      {
         $current_stock['value']=$new_stock;
      }
   }
}

$fruits=array(
   "apple" => new Fruit("Apple"),
   "peach" => new Fruit("Peach")
);

//Run!

foreach($fruits as $fruit)
{
   $fruit->setStock(8);
   echo $fruit->name." = ".$fruit->stock['current']['value']."\n";
}

foreach($fruits as $fruit)
{
   $fruit->stock['current']['value']=8;
   $fruit->stock['cache']['value']=8;
   echo $fruit->name." = ".$fruit->stock['current']['value']."\n";
}

Este código saca por pantalla:

Apple = 3
Peach = 3
Apple = 8
Peach = 8

¿Por qué en el primer foreach no funciona el setStock a 8? ¿Por qué en el segundo sí? Dad una explicación y proponed alguna solución para arreglarlo. El premio para el primero que acierte es, como de costumbre, es una botella de kilkenny 😉

kilkenny-02-300x251


Si te gusta esta entrada... compártela! 😉
Tweet about this on TwitterShare on Facebook0Share on Google+0Share on Tumblr0Pin on Pinterest0Share on LinkedIn0Share on Reddit0

4 Comentarios

  1. Foreach recoge para iterar, si no le especificas nada, los valores del array que recorre por valor y, siendo esos valores otros arrays, lo que devuelve son copias de esos arrays (que es el quid de la cuestión, porque si php los tratase como objetos – véase $fruit en el último foreach – o direcciones de memoria del primer elemento, no habría follón). Bastaría con un ampersand (&) en algún lugar… Me suena que delante de $current_stock, para que los recogiera por referencia y pudiera modificar los originales.

    Pero total, si luego la Kilkenny no es de barril… xD

    • LaNsHoR

      Premio! Con algunas aclaraciones:

      En este “resultado inesperado” influyen dos aspectos poco comunes:

      1) El primero, es que un foreach en php asigna por copia los tipos básicos y por referencia el resto. Un comportamiento único para una estructura de control que en principio está reservado sólo para los cambios de ámbito.

      2) El segundo, es que los arrays se pasan por copia en php como si fuera un tipo básico (dejo una pregunta en el aire: ¿lo son en todos los aspectos?). Esto se vuelve aún más confuso si en el array almacenamos objetos, ya que aunque el array se pasa por copia, copiamos las referencias y al usar los valores saltamos automáticamente a ellas; cuyo efecto final obvio es idéntico a pasar los objectos por referencia. Si mezclamos en un mismo array objectos con tipos_básicos/arrays no podremos seguir el mismo patrón de mutabilidad en el código y tendremos que contemplar ambas opciones.

      Soluciones:

      a) Como dice Erful, usar & delante de la variable de iteración para forzar al paso por referencia.

            foreach($this->stock as $key
                    => &$current_stock)
            {
               $current_stock['value']=$new_stock;
            }
      

      b) No usar el valor de copia y sí “la llave” para acceder al valor original:

            foreach($this->stock as $key
                    => $current_stock)
            {
               $this->stock[$key]=$new_stock;
            }
      

      El primer método es el más elegante, pero si usamos foreachs anidados es menos seguro y recomendado; ya que modificaciones en las referencias iniciales pueden dar resultados indeterminados en el orden de iteración (esa es la razón de que originalmente el foreach se comporte como se comporta).

      • StormByte

        Tengo una pequeña aclaración:
        (…) El segundo, es que los arrays se pasan por copia en php (…)

        Cualquier paso de parámetros en PHP, se realiza (internamente) referencia. Cuando no se especifica el paso por referencia, PHP utiliza COW (Copy On Write), que solo crea una copia si alguna cosa se ha modificado.
        En caso contrario, tendremos siempre la referencia al array/dato/objeto original.

        En la práctica, y en este problema en concreto, sí se está modificando esa referencia interna, así que efectivamente, obtenemos una copia al vuelo, ya modificada.

        Una lectura interesante sobre esto, es: https://www.research.ibm.com/trl/people/mich/pub/200901_popl2009phpsem.pdf y buscar “php copy on write” en google.

      • StormByte

        Ejemplos:

        Ejemplo 1
        =========
        $a=”Hola”;
        $b=$a;
        ¿Son $a y $b distintos? NO: $b contiene una referencia exacta a $a

        Ejemplo 2
        =========
        $a=”Hola”;
        $b=$a;
        $a=”Pérez”;
        ¿Son $a y $b distintos? SI: $b contenía una referencia exacta a $a, pero al haber sido modificado $a, mediante copy on write, se ha creado una copia en $b de lo que antes contenía $a

Responder

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Límite de tiempo se agote. Por favor, recargar el CAPTCHA por favor.