Foros del Web » Programando para Internet » PHP »

[SOLUCIONADO] Extraño comportamiento de foreach

Estas en el tema de Extraño comportamiento de foreach en el foro de PHP en Foros del Web. Que tal, por un error en mi codigo descubri esta curiosidad del foreach: @import url("http://static.forosdelweb.com/clientscript/vbulletin_css/geshi.css"); Código PHP: Ver original <?php     class A   ...
  #1 (permalink)  
Antiguo 11/08/2014, 22:09
Avatar de NSD
NSD
Colaborador
 
Fecha de Ingreso: mayo-2012
Ubicación: Somewhere
Mensajes: 1.332
Antigüedad: 11 años, 11 meses
Puntos: 320
Extraño comportamiento de foreach

Que tal, por un error en mi codigo descubri esta curiosidad del foreach:

Código PHP:
Ver original
  1. <?php
  2.     class A
  3.     {
  4.         public $datos = [1, 2, 3, 4, 5];
  5.        
  6.         public function __destruct()
  7.         {
  8.             echo("destruido");
  9.         }
  10.     }
  11.    
  12.     $test = new A();
  13.     foreach($test->datos as $test)
  14.     {
  15.         var_dump($test);
  16.     }    
  17.    
  18.     var_dump($test);

Produce la salida:
Cita:
destruido
int 1
int 2
int 3
int 4
int 5
int 5
¿Acaso el foreach itera una propiedad de un objeto destruido?
En la version serbia del manual (no en las otras ) esta la siguiente nota:
Cita:
Напомена:
Unless the array is referenced, foreach operates on a copy of the specified array and not the array itself. foreach has some side effects on the array pointer. Don't rely on the array pointer during or after the foreach without resetting it.
¿Eso significa que si tengo un array inmenso, php lo duplicara para iterarlo?
para comprobarlo hice esto:
Código PHP:
Ver original
  1. <?php
  2.     class A
  3.     {
  4.         public $datos = [];
  5.        
  6.         public function __destruct()
  7.         {
  8.             echo("destruido");
  9.         }
  10.     }
  11.    
  12.     $test = new A();
  13.     for($i=0; $i<999999; $i++)
  14.         $test->datos[] = $i;  
  15.        
  16.    
  17.     foreach($test->datos as $test)
  18.     {
  19.         var_dump(memory_get_usage());
  20.         break;
  21.     }      
  22.    
  23.     var_dump($test);

la salida es esta:
Cita:
int 84340000
destruido
int 84339928
int 145592
int 0
por lo visto, el consumo de memoria baja dentro del foreach, por lo que no lo esta duplicando, entonces ¿que es lo que hace realmente?
__________________
Maratón de desafíos PHP Junio - Agosto 2015 en FDW | Reglamento - Desafios
  #2 (permalink)  
Antiguo 11/08/2014, 22:15
Avatar de Triby
Mod on free time
 
Fecha de Ingreso: agosto-2008
Ubicación: $MX->Gto['León'];
Mensajes: 10.106
Antigüedad: 15 años, 8 meses
Puntos: 2237
Respuesta: Extraño comportamiento de foreach

El destructor se está ejecutando antes de ingresar a foreach, porque solo estás haciendo un echo, en caso de ser mostrado con var_dump() sería algo como string...

En todo caso, habrá que investigar el comportamiento del destructor.
__________________
- León, Guanajuato
- GV-Foto
  #3 (permalink)  
Antiguo 12/08/2014, 11:57
Avatar de hhs
hhs
Colaborador
 
Fecha de Ingreso: junio-2013
Ubicación: México
Mensajes: 2.995
Antigüedad: 10 años, 9 meses
Puntos: 379
Respuesta: Extraño comportamiento de foreach

Código PHP:
Ver original
  1. <?php
  2.     class A
  3.     {
  4.         public $datos = [1, 2, 3, 4, 5];
  5.        
  6.         public function __destruct()
  7.         {
  8.             echo("destruido");
  9.         }
  10.     }
  11.    
  12.     $test = new A();
  13.     foreach($test->datos as $test)
  14.     {
  15.         var_dump($test);
  16.     }    
  17.    
  18.     var_dump($test);
Lamento informar que este es el comportamiento esperado
  • El alcance de $test es global
  • foreach es una estructura
  • Destruct se llama de forma natural al final del script o donde se haga la ultima referencia al objeto
La ultima referencia al objeto es cuando se pasa el contenido de la propiedad datos al foreach es cuando se hace la llamada a destruct. y esto pasa por que en el foreach asigna cada valor de la iteración en $test (es global ) que ya no contiene el objeto si no 1 en la primera iteración por eso al final el ultimo var_dump tiene el valor de 5.
El segundo caso:
Código PHP:
Ver original
  1.    
  2.     foreach($test->datos as $test)
  3.     {
  4.         var_dump(memory_get_usage());
  5.         break; // Ojo con esto
  6.     }      
  7.    
  8.     var_dump($test);
Es similar pero aquí como no se itera solo una vez ya que el break finaliza el foreach de forma prematura, se pierde el arreglo liberando memoria.
Lo compruebas fácilmente ya que $test en el ultimo var_dump tiene el valor de cero.
Para solucionar el extraño "comportamiento" usa otro nombre de variable.
Código PHP:
Ver original
  1. class A
  2. {
  3.     public $datos = [1, 2, 3, 4, 5];
  4.  
  5.     public function __destruct()
  6.     {
  7.         echo("destruido");
  8.     }
  9. }
  10.  
  11. $test = new A();
  12.  
  13. var_dump($test);
  14.  
  15. foreach($test->datos as $dato)
  16. {
  17.     var_dump($dato);
  18. }
  19.  
  20. var_dump($test);
La salida se comporta como esperabas:
Cita:
object(A)[1]
public 'datos' =>
array (size=5)
0 => int 1
1 => int 2
2 => int 3
3 => int 4
4 => int 5

int 1

int 2

int 3

int 4

int 5

object(A)[1]
public 'datos' =>
array (size=5)
0 => int 1
1 => int 2
2 => int 3
3 => int 4
4 => int 5

destruido
Creo que esto será una buena pregunta para examen
__________________
Saludos
About me
Laraveles
A class should have only one reason to change.
  #4 (permalink)  
Antiguo 12/08/2014, 13:12
Avatar de NSD
NSD
Colaborador
 
Fecha de Ingreso: mayo-2012
Ubicación: Somewhere
Mensajes: 1.332
Antigüedad: 11 años, 11 meses
Puntos: 320
Respuesta: Extraño comportamiento de foreach

Gracias a ambos por las respuestas.

@triby estube revisando el manual y haciendo unas pruebas sobre los destructores, pero no hay mucho mas en su comportamiento de lo que menciona @hhs, se invocan cuando el objeto deja de ser necesario (al final del script, cuando se llama a unset, cuando se le asigna un valor o cuando ya no se lo referencia mas)

@hhs interesante el analisis pero tengo una duda con esta parte de tu razonamiento:
Cita:
$test (es global ) que ya no contiene el objeto si no 1 en la primera iteración por eso al final el ultimo var_dump tiene el valor de 5.
Eso no es tan asi (o no me doy cuenta al menos, espero me tengas paciencia), considera el siguiente ejemplo similar al primero:

Código PHP:
Ver original
  1. <?php
  2.     error_reporting(E_ALL);
  3.    
  4.     class A implements Iterator
  5.     {
  6.         private $datos = [1, 2, 3, 4, 5];
  7.  
  8.         public function rewind() { reset($this->datos); }
  9.         public function current() { return current($this->datos); }
  10.         public function key() { return key($this->datos); }
  11.         public function next() { return next($this->datos); }
  12.         public function valid() { return (key($this->datos) !== NULL && key($this->datos) !== FALSE); }
  13.         public function __destruct()
  14.         {
  15.             echo("destruido");
  16.         }
  17.     }
  18.  
  19.     $test = new A();
  20.     foreach($test as $test)
  21.     {    
  22.         var_dump($test);
  23.     }
  24.     var_dump($test);

La salida que produce es:
Cita:
int 1
int 2
int 3
int 4
int 5
destruido
int 5
Por lo tanto, la variable $test cambia su valor dentro del foreach, pero el objeto sigue "vivo" ya que se esta haciendo uso de sus metodos, solo es destruido al finalizar el foreach.

Pero este este script en cambio:
Código PHP:
Ver original
  1. class A implements IteratorAggregate
  2.     {
  3.         private $datos = [1, 2, 3, 4, 5];
  4.  
  5.         public function getIterator() { return new ArrayIterator($this->datos); }
  6.         public function __destruct()
  7.         {
  8.             echo("destruido");
  9.         }
  10.     }
  11.  
  12.     $test = new A();
  13.     foreach($test as $test)
  14.     {    
  15.         var_dump($test);
  16.     }
  17.     var_dump($test);

produce la salida:
Cita:
destruido
int 1
int 2
int 3
int 4
int 5
int 5
exactamente la misma que si iterara la propiedad publica directamente (lo cual es coherente).

Cita:
Para solucionar el extraño "comportamiento" usa otro nombre de variable.
Si, por supuesto, por error escribi el mismo nombre en ambas partes, el script funciono durante mucho tiempo asi, pero aller tube que actualizarlo y al verlo me llamo la atencion porque a priori supuse que eso no funcionaria.
Quiero entender que es lo que esta ocurriendo mas alla de que la solucion sea cambiar el nombre, porque dependiendo de cual sea el comportamiento puede ser (por ejemplo) una buena forma de optimizar la carga de un listado ya que php iria liberando la memoria a medida que produce la salida en vez de mantenerla ocupada hasta terminar de mostrar todo.

Seria interesante combinar esto con los generadores de alguna forma...
__________________
Maratón de desafíos PHP Junio - Agosto 2015 en FDW | Reglamento - Desafios
  #5 (permalink)  
Antiguo 12/08/2014, 13:19
Avatar de pateketrueke
Modernizr
 
Fecha de Ingreso: abril-2008
Ubicación: Mexihco-Tenochtitlan
Mensajes: 26.399
Antigüedad: 16 años
Puntos: 2534
Respuesta: Extraño comportamiento de foreach

Creo que no entiendes como funciona el foreach() entonces.

Cita:
Por lo tanto, la variable $test cambia su valor dentro del foreach, pero el objeto sigue "vivo" ya que se esta haciendo uso de sus metodos, solo es destruido al finalizar el foreach.
$test no contiene ninguna referencia al objecto, es una copia del último valor que se iteró en el foreach() y nada más.

Código PHP:
Ver original
  1. $foo = array('candy', 'bar');
  2.  
  3. foreach ($foo as $bar); // sin {}
  4.  
  5. echo $bar; // bar

¿Se entiende?
__________________
Y U NO RTFM? щ(ºдºщ)

No atiendo por MP nada que no sea personal.
  #6 (permalink)  
Antiguo 12/08/2014, 16:48
Avatar de NSD
NSD
Colaborador
 
Fecha de Ingreso: mayo-2012
Ubicación: Somewhere
Mensajes: 1.332
Antigüedad: 11 años, 11 meses
Puntos: 320
Respuesta: Extraño comportamiento de foreach

Cita:
$test no contiene ninguna referencia al objecto, es una copia del último valor que se iteró en el foreach() y nada más.
Pero $test era el objeto, en ejemplo que me pones no pasa nada raro porque el nombre de la variable a iterar y la variable que se usa en la iteracion son diferentes, es lo que menciono @hhs, mi duda es cuando le das a los dos el mismo nombre
__________________
Maratón de desafíos PHP Junio - Agosto 2015 en FDW | Reglamento - Desafios
  #7 (permalink)  
Antiguo 12/08/2014, 16:58
Avatar de pateketrueke
Modernizr
 
Fecha de Ingreso: abril-2008
Ubicación: Mexihco-Tenochtitlan
Mensajes: 26.399
Antigüedad: 16 años
Puntos: 2534
Respuesta: Extraño comportamiento de foreach

Cita:
Iniciado por NSD Ver Mensaje
Pero $test era el objeto, en ejemplo que me pones no pasa nada raro porque el nombre de la variable a iterar y la variable que se usa en la iteracion son diferentes, es lo que menciono @hhs, mi duda es cuando le das a los dos el mismo nombre
Te equivocas, $test es el valor que devuelve al extraer cada elemento dentro del foreach(), porque primero se hace una copia de $test->datos y a partir de ahí se sobre escribe $test por lo cual ya no representa al objeto.
__________________
Y U NO RTFM? щ(ºдºщ)

No atiendo por MP nada que no sea personal.
  #8 (permalink)  
Antiguo 12/08/2014, 17:19
Avatar de Triby
Mod on free time
 
Fecha de Ingreso: agosto-2008
Ubicación: $MX->Gto['León'];
Mensajes: 10.106
Antigüedad: 15 años, 8 meses
Puntos: 2237
Respuesta: Extraño comportamiento de foreach

Creo entender el punto expuesto por @pateketrueke

Código PHP:
Ver original
  1. foreach($test->datos as $test) {
  2.      // $test sobreescribe al objeto y, por tanto, llama al destructor
  3.      // Las iteraciones son sobre valores que contenía $test->datos
  4. }
  5.  
  6. foreach($test->datos as $dato) {
  7.     // Aquí no se sobreescribe el objeto y no debería aparecer "Destruido"
  8. }
__________________
- León, Guanajuato
- GV-Foto
  #9 (permalink)  
Antiguo 12/08/2014, 21:00
Avatar de NSD
NSD
Colaborador
 
Fecha de Ingreso: mayo-2012
Ubicación: Somewhere
Mensajes: 1.332
Antigüedad: 11 años, 11 meses
Puntos: 320
Respuesta: Extraño comportamiento de foreach

Cita:
Te equivocas, $test es el valor que devuelve al extraer cada elemento dentro del foreach(), porque primero se hace una copia de $test->datos y a partir de ahí se sobre escribe $test por lo cual ya no representa al objeto.
Si el objeto se copiara se estaria duplicando, por lo tanto deberia ocupar el doble de memoria, sin embargo, unas de las pruebas hechas muestra que la memoria baja dentro del foreach, al llamarse al destructor se destruye el objeto pero la propiedad sigue existiendo ya que es iterada.

Cita:
foreach($test->datos as $test) {
// $test sobreescribe al objeto y, por tanto, llama al destructor
// Las iteraciones son sobre valores que contenía $test->datos
}
En el primer ejemplo de este mensaje: http://www.forosdelweb.com/f18/extra...0/#post4627420 no estoy iterando la propiedad, sino el objeto mismo a travez se sus metodos, sin embargo la variable se sobreescribe pero todo el objeto sigue existiendo en algun lado y solo es destruido luego de toda la iteracion.

Como si el elemento a iterar no se destruyera.

En la seccion de objetos y referencias esta esto:
Cita:
Desde PHP 5, una variable de tipo objeto ya no contiene el objeto en sí como valor. Únicamente contiene un identificador del objeto que le permite localizar al objeto real. Cuando se pasa un objeto como parámetro, o se devuelve como retorno, o se asigna a otra variable, las distintas variables no son alias: guardan una copia del identificador, que apunta al mismo objeto.
El objeto solo se destruye cuando no hay mas identificadores definidos de ese objeto.

¿Es posible que el foreach realice una copia de ese identificador y por lo tanto pueda seguir iterando aunque la variable original ya haya cambiado de valor?
__________________
Maratón de desafíos PHP Junio - Agosto 2015 en FDW | Reglamento - Desafios
  #10 (permalink)  
Antiguo 13/08/2014, 17:04
Avatar de hhs
hhs
Colaborador
 
Fecha de Ingreso: junio-2013
Ubicación: México
Mensajes: 2.995
Antigüedad: 10 años, 9 meses
Puntos: 379
Respuesta: Extraño comportamiento de foreach

Cita:
Eso no es tan asi (o no me doy cuenta al menos, espero me tengas paciencia), considera el siguiente ejemplo similar al primero:
Código PHP:
Ver original
  1. $test = new A();
  2.     foreach($test as $test)
  3.     {    
  4.         var_dump($test);
  5.     }
  6.     var_dump($test);
No se ve a simple vista, pero el $test que imprimes dentro del foreach ya no contiene el identificador del objeto por una simple y sencilla razón que estas pasando por alto. La variable $test que pasas al foreach y el que regresa están en el mismo ámbito debido a que el foreach no es una función $test no esta en un ámbito local, asi que la sobre escribe.
Entonces que es lo que pasa cuando iteras el objeto ?

Hasta aqui ya sabemos que $test se sobre escribe pero aun asi se itera el objeto y la razón ya te la respondiste.
Cita:
El objeto solo se destruye cuando no hay mas identificadores definidos de ese objeto.

¿Es posible que el foreach realice una copia de ese identificador y por lo tanto pueda seguir iterando aunque la variable original ya haya cambiado de valor?
Php trata de forma diferente a los objetos por que su "referencia" se hace por el identificador que pasa a otras variables, por lo cual el foreach a nivel interno tiene que mantenerlo para no crear efectos adversos ya que el objeto puede estar "referenciado" en otras variables.
Esto lo comprobamos de forma sencilla:
Código PHP:
Ver original
  1. class A implements Iterator
  2. {
  3.     private $datos = [1, 2, 3, 4, 5];
  4.  
  5.     public function rewind() { reset($this->datos); }
  6.     public function current() { return current($this->datos); }
  7.     public function key() { return key($this->datos); }
  8.     public function next() { return next($this->datos); }
  9.     public function valid() { return (key($this->datos) !== NULL && key($this->datos) !== FALSE); }
  10.     public function __destruct()
  11.     {
  12.         echo("destruido");
  13.     }
  14. }
  15.  
  16. $test = new A();
  17. $test1 = $test; //pasamos identificador
  18. foreach($test as $test)
  19. {
  20.     var_dump($test);
  21. }
  22. var_dump($test); //se sobre escribe; ahora vale 5
  23. var_dump($test1); //se mantiene el objeto
  24. echo 'Depues de esto se llama a __destruct <br>';
Despues de foreach el objeto original sigue con vida aun después de finalizar el ciclo y como se puede comprobar nuevamente $test se sobre escribe.
Si aun existe duda de si $test mantiene o no la referencia podemos probar algo: tu puedes llamar a __destruct si usas unset
Código PHP:
Ver original
  1. class A implements Iterator
  2. {
  3.     private $datos = [1, 2, 3, 4, 5];
  4.  
  5.     public function rewind() { reset($this->datos); }
  6.     public function current() { return current($this->datos); }
  7.     public function key() { return key($this->datos); }
  8.     public function next() { return next($this->datos); }
  9.     public function valid() { return (key($this->datos) !== NULL && key($this->datos) !== FALSE); }
  10.     public function __destruct()
  11.     {
  12.         echo("destruido");
  13.     }
  14. }
  15.  
  16. $test = new A();
  17. $test1 = $test; //pasamos identificador
  18. foreach($test as $test)
  19. {
  20.     var_dump($test);
  21.     unset($test);
  22. }
  23. var_dump($test); //se sobre escribe; ahora vale 5
  24. var_dump($test1); //se mantiene el objeto
  25. echo 'Depues de esto se llama a __destruct <br>';
Si el $test sigue con el identificador debiera de detener de forma prematura el foreach al destruir el objeto y por ende tendríamos un resultado inesperado en $test1. Pero no es asi si ejecutas esto obtienes lo siguiente:
Cita:
int 1
int 2
int 3
int 4
int 5


( ! ) Notice: Undefined variable: test in /home/hheredia/public_html/Curso/iteracion.php on line 56
Call Stack
# Time Memory Function Location
1 0.0022 236176 {main}( ) ../iteracion.php:0

null

object(A)[1]
private 'datos' =>
array (size=5)
0 => int 1
1 => int 2
2 => int 3
3 => int 4
4 => int 5

Depues de esto se llama a __destruct
destruido
$test1 sigue integro con su referencia al objeto como es de esperarse junto con el notice de variable indefinida de $test.
__________________
Saludos
About me
Laraveles
A class should have only one reason to change.
  #11 (permalink)  
Antiguo 14/08/2014, 13:17
Avatar de NSD
NSD
Colaborador
 
Fecha de Ingreso: mayo-2012
Ubicación: Somewhere
Mensajes: 1.332
Antigüedad: 11 años, 11 meses
Puntos: 320
Respuesta: Extraño comportamiento de foreach

Tienes razon, muchas gracias por resolver mi duda, como para cerrar y completar lo dicho (que esta muy claro) hice este codigo:

Código PHP:
Ver original
  1. <?php
  2.     error_reporting(E_ALL);
  3.    
  4.     class A implements Iterator
  5.     {
  6.         public $datos = [1, 2, 3, 4, 5];
  7.  
  8.         public function rewind() { reset($this->datos); }
  9.         public function current() { return current($this->datos); }
  10.         public function key() { return key($this->datos); }
  11.         public function next() { return next($this->datos); }
  12.         public function valid() { return (key($this->datos) !== NULL && key($this->datos) !== FALSE); }
  13.         public function __destruct()
  14.         {
  15.             echo("destruido");
  16.         }
  17.     }
  18.    
  19.     $test = new A();
  20.     $instancia = $test;
  21.     debug_zval_dump($instancia);
  22.     foreach($instancia as $test)
  23.     {    
  24.         debug_zval_dump($instancia);
  25.         var_dump($test);
  26.     }
  27.     var_dump($test);

cuya salida es:
Cita:
object(A)#1 (1) refcount(3){ ["datos"]=> array(5) refcount(1){ [0]=> long(1) refcount(2) [1]=> long(2) refcount(2) [2]=> long(3) refcount(2) [3]=> long(4) refcount(2) [4]=> long(5) refcount(2) } } object(A)#1 (1) refcount(3){ ["datos"]=> array(5) refcount(1){ [0]=> long(1) refcount(2) [1]=> long(2) refcount(2) [2]=> long(3) refcount(2) [3]=> long(4) refcount(2) [4]=> long(5) refcount(2) } }
int 1
object(A)#1 (1) refcount(3){ ["datos"]=> array(5) refcount(1){ [0]=> long(1) refcount(2) [1]=> long(2) refcount(2) [2]=> long(3) refcount(2) [3]=> long(4) refcount(2) [4]=> long(5) refcount(2) } }
int 2
object(A)#1 (1) refcount(3){ ["datos"]=> array(5) refcount(1){ [0]=> long(1) refcount(2) [1]=> long(2) refcount(2) [2]=> long(3) refcount(2) [3]=> long(4) refcount(2) [4]=> long(5) refcount(2) } }
int 3
object(A)#1 (1) refcount(3){ ["datos"]=> array(5) refcount(1){ [0]=> long(1) refcount(2) [1]=> long(2) refcount(2) [2]=> long(3) refcount(2) [3]=> long(4) refcount(2) [4]=> long(5) refcount(2) } }
int 4
object(A)#1 (1) refcount(3){ ["datos"]=> array(5) refcount(1){ [0]=> long(1) refcount(2) [1]=> long(2) refcount(2) [2]=> long(3) refcount(2) [3]=> long(4) refcount(2) [4]=> long(5) refcount(2) } }
int 5
int 5
destruido
alli se ve que la cantidad de referencias al objeto aumenta dentro del foreach (sigue igual, pero porque una se elimina al hacer la asignacion) confirmándose lo que dices.

Saludos.
__________________
Maratón de desafíos PHP Junio - Agosto 2015 en FDW | Reglamento - Desafios
  #12 (permalink)  
Antiguo 14/08/2014, 13:46
Avatar de hhs
hhs
Colaborador
 
Fecha de Ingreso: junio-2013
Ubicación: México
Mensajes: 2.995
Antigüedad: 10 años, 9 meses
Puntos: 379
Respuesta: Extraño comportamiento de foreach

Gracias a ti NSD que expones situaciones como esta, que sirven para exponer como funciona el lenguaje en situaciones poco frecuentes. Además ayudan a tener días fuera de lo habitual en el foro.
__________________
Saludos
About me
Laraveles
A class should have only one reason to change.

Etiquetas: comportamiento, extraño, foreach
Atención: Estás leyendo un tema que no tiene actividad desde hace más de 6 MESES, te recomendamos abrir un Nuevo tema en lugar de responder al actual.
Respuesta




La zona horaria es GMT -6. Ahora son las 07:29.