Pbcopie de tableau après utilisation de références -COMPLEXE

Administrateur PHPfrance
Administrateur PHPfrance | 3088 Messages

29 juin 2007, 21:49

Ok, donc j'ai exécuté le script du premier message, et en effet le résultat n'est pas évident à première vue. En réduisant le code à sa plus simple expression on peut plus précisément identifier d'où vient le problème:
// un tableau de démo 
$tree['one'] = '__LEAF__';

// on crée une référence vers lui-même
$tree['one'] =& $tree['one'];

// copy le tableau 
$copy = $tree; 

// Modifie la feuille 
$tree['one'] = 'ALTER'; 

// Affiche le tableau d'origine et la copy 
print_r($tree); 
print_r($copy);
Ce n'est pas forcément évident, mais c'est dû à la nature des références en PHP. J'ai mis quelques années avant de connaître leur vraie nature et aujourd'hui encore il me reste des lacunes sur leur fonctionnement intrinsèque donc ne vous affolez pas si vous n'étiez pas au courant :lol: Je ne sais pas si on peut le résumer facilement, mais si je le ferais comme ça :
  • toutes les données ("truc", 42, false, etc...) en PHP sont dans des "containers"
  • toute variable ($var) se voit assigner un container et possède un indicateur pour mémoriser s'il s'agit d'une référence ou d'une valeur
  • chaque container possède un compteur pour mémoriser le nombre de variables qui lui sont assignées et quand ce compteur tombe à zéro le container est détruit
Quand on fait
$tree['one'] =& $tree['one'];
...on dit à PHP "assigne par référence à la variable $tree['one'], le container #xyz de la variable $tree['one']". PHP s'exécute et le lien avec le container devient alors une référence. [note: ça ne fonctionne que pour les éléments d'une variable de type composé, donc les tableaux et les objets]

Là où ça se corse, c'est que quand on copie un tableau (et apparemment, quand on clone un objet), toutes les valeurs sont copiées en tant que valeurs, et toutes les références en tant que références. Donc quand on fait une copie du tableau $tree, son élément "one" est créé en tant que référence vers le container #xyz et leur destin est alors scellé.

Je ne sais pas si c'est plus clair, mais si vous avez des doutes et que vous lisez l'anglais je ne saurais trop vous conseiller de lire cet article de Derick Rethans sur les références en PHP, tiré du php|architect de juin 2005.

ViPHP
ViPHP | 2287 Messages

29 juin 2007, 23:44

Hubert, relis bien le code du début du sujet et tous les tests présentés. On parle ici de référence sur le tableau et non pas à l'intérieur (comme dans le bug que pointe Mandar et sur lequel Zeev a laissé un commentaire qui rejoignait ce que tu dis et qui laissait comprendre DIY - "Do It Yourself").

En revanche l'article de Derick Rethans que tu pointes comporte un paragraphe et un schéma qui reproduisent et expliquent très bien le problème sans toutefois souligner que c'en est un (Figure 2, page 3 dans le PDF, page 38 du magazine).

Il faut rentrer assez profondément dans les mécanismes de gestion de mémoire internes au Zend Engine, expliqués dans le PDF, pour comprendre pourquoi une même ligne de code de simple affectation peut soit créer une référence soit une vraie copie (cas illustré dans les tests que nous nous étions échangés plus haut), et il s'agit bien d'un bug dans le sens où le codeur php lambda peut être dérouté par ce code qui ne produit pas forcément le résultat qu'on serait en droit d'attendre, à savoir une copie.

Je pense que si un correctif n'est pas envisagé pour cela par les développeurs du langage, au moins l'ajout d'un message d'avertissement (et pourquoi pas en E_NOTICE...) serait une idée judicieuse pour au moins informer l'utilisateur que ce code est sujet à une interprétation pas toujours heureuse.

Par exemple quelquechose comme : Notice: Assigning by value an already referenced variable may lead to unexpected references [inside] of the target variable - in /home/calimero/php/php_array_cloning.php on line 50
if(!@work()){ Nespresso(); } else { what(); }
______________________________

Administrateur PHPfrance
Administrateur PHPfrance | 3088 Messages

30 juin 2007, 01:39

On parle ici de référence sur le tableau et non pas à l'intérieur
Ben en fait le problème vient surtout du fait que le tableau est composé de références (enfin, son seul élément en tout cas). Ensuite il se conjugue avec le mécanisme de copie des tableaux, mais si on n'utilise pas de références dans le tableau tout fonctionne comme on peut s'y attendre.

Pour l'avertissement je doute que ça n'arrive jamais dans la mesure où il ne s'agit pas d'un bug per se et il y a certainement des utilisations légitimes. Quand je fais une copie d'un tableau contenant des références, je sais que les références sont préservées et je n'aimerais pas trop que PHP génère une erreur alors que mon code fonctionne comme je le souhaite. Pour pousser le raisonnement à l'absurde, c'est comme si PHP générait un avertissement lorsque tu assignes un objet "par valeur", pour te prévenir qu'il s'agit en fait du même objet.

Donc voilà, c'est juste un fonctionnement par forcément évident, il suffit juste de s'en rappeler.

Administrateur PHPfrance
Administrateur PHPfrance | 3088 Messages

30 juin 2007, 01:53

Ah, j'ai compris pourquoi on voit le problème différemment. Pour reprendre le commentaire du premier message:
// parcour le tableau jusqu'a la premiere feuille  
// Ca n'alter rien ca parcours, a la fin du while, echo $pointer retournerai __LEAF__
En fait si, ça altère bien le tableau. Remplacez print_r() par var_dump() et vous verrez que la nature du tableau a changé. Et du coup:
Comment faire pour obtenir une copie
Là aussi, pour moi $copy est une copie de $tree, à condition d'utiliser la définition de PHP concernant les copies de tableau :)

Au passage, j'en ai profité pour chercher comment déréférencer l'élément pointé par $pointer... en fait il suffit de suivre la bonne pratique que j'évoquais plus haut. En supprimant $pointer à la sortie de la boucle, l'élément redevient une valeur et la copie fonctionne comme prévue.

Mammouth du PHP | 505 Messages

30 juin 2007, 14:32

Parfois, c'est pas si facile de déréférencer.

Regarde le code suivant :
<?php
// un tableau de démo
$tree['one']['two']['three'] = '__LEAF__';
// On affiche le contenu
echo "le tree de base =>",var_dump($tree);

// crée un pointer sur le tableau
$pointer = &$tree;

// parcour le tableau jusqu'a la premiere feuille 
// Ca n'alter rien ca parcours, a la fin du while, echo $pointer retournerai __LEAF__
while(is_array($pointer)) {
	$key = key($pointer);
	echo "inwhile key = $key & \$tree=>", var_dump($tree);
    $pointer = &$pointer[$key];
}
echo "before unset =>", var_dump($pointer,$tree);
unset($pointer);
echo "after unset =>", var_dump($pointer,$tree);
// copy le tableau
$copy = $tree;

// Modifie la feuille
$tree['one']['two']['three'] = 'ALTER';
// Affiche le tableau d'origine et la copy
echo "tree => ",var_dump($tree);
echo "copy => ",var_dump($copy); 
et le résultat

Code : Tout sélectionner

le tree de base =>array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } inwhile key = one & $tree=>array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } inwhile key = two & $tree=>array(1) { ["one"]=> &array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } inwhile key = three & $tree=>array(1) { ["one"]=> &array(1) { ["two"]=> &array(1) { ["three"]=> string(8) "__LEAF__" } } } before unset =>string(8) "__LEAF__" array(1) { ["one"]=> array(1) { ["two"]=> &array(1) { ["three"]=> &string(8) "__LEAF__" } } } after unset =>NULL array(1) { ["one"]=> array(1) { ["two"]=> &array(1) { ["three"]=> string(8) "__LEAF__" } } } tree => array(1) { ["one"]=> array(1) { ["two"]=> &array(1) { ["three"]=> string(5) "ALTER" } } } copy => array(1) { ["one"]=> array(1) { ["two"]=> &array(1) { ["three"]=> string(5) "ALTER" } } }
On constate a la sortie du while que $tree contient 2 références... Je cherche encore pourquoi...
et après le unset, il n'en contient plus qu'une, mais c'est encore une de trop. Du coup, on ne peut toujour pas dupliquer $tree a la sortie.

Pour contourner ca, j'ai fais le code suivant. Je suis obliger de passer par une variable intermédiaire pour pouvoir faire un unset de $pointer avant de le mettre a jour... Super pratique...



<?php
// un tableau de démo
$tree['one']['two']['three'] = '__LEAF__';
// On affiche le contenu
echo "le tree de base =>",var_dump($tree);

// crée un pointer sur le tableau
$pointer = &$tree;

// parcour le tableau jusqu'a la premiere feuille 
// Ca n'alter rien ca parcours, a la fin du while, echo $pointer retournerai __LEAF__
while(is_array($pointer)) {
	$key = key($pointer);
	echo "inwhile key = $key & \$tree=>", var_dump($tree);
	// Utilise une var tampon
    $pointer2 = &$pointer[$key];
    // on supprime la référence
    unset($pointer);
    // On réafect a pointer
    $pointer = $pointer2;
    // On supprime la var tampon puisque c'est aussi une référence
    unset($pointer2);
}
echo "before unset =>", var_dump($pointer,$tree);
unset($pointer);
echo "after unset =>", var_dump($pointer,$tree);
// copy le tableau
$copy = $tree;

// Modifie la feuille
$tree['one']['two']['three'] = 'ALTER';
// Affiche le tableau d'origine et la copy
echo "tree => ",var_dump($tree);
echo "copy => ",var_dump($copy); 

Et la voila le résultat.

Code : Tout sélectionner

X-Powered-By: PHP/5.2.0 Content-type: text/html; charset=iso-8859-15 le tree de base =>array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } inwhile key = one & $tree=>array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } inwhile key = two & $tree=>array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } inwhile key = three & $tree=>array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } before unset =>string(8) "__LEAF__" array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } after unset =>NULL array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } } tree => array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(5) "ALTER" } } } copy => array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(8) "__LEAF__" } } }
J'ai bien obtenu une copy de mon arbre...

Reste la question, pourquoi y avait il 2 références, d'autant que meme si l'arbre est plus profond, il n'y en a tjs que 2 qui glisse vers le fond...

Au final, j'ai une solution plus propre en utilisant la SPL, mais la conso ram n'est pas la meme et il faut que je vérifie si cela s'applique dans mon contexte (parcequeici, c'est une version simplifiée du problème :) )

Le code avec la SPL
<?php

$tree = new ArrayObject(array('one'=>array('two'=>array('three'=>'LEAF'))));
var_dump($tree);

// crée  pointer sur le tableau (c'est un objet donc passage par réf)
$pointer = $tree;

var_dump($tree,$pointer);
while(is_array(current($pointer))) {
	$key = key($pointer);
	$pointer = new ArrayObject($pointer->offsetGet($key));
	echo "inwhile key = $key & \$tree=>", var_dump($pointer,$tree);
}

echo "before unset =>", var_dump($pointer,$tree);
unset($pointer);
echo "after unset =>", var_dump($pointer,$tree);

// Pour la copie, Au choix 
// soit cette solution
$copy =  $tree->getArrayCopy();
// ou 
// $copy = clone $tree;


// Modifie la feuille
$tree['one']['two']['three'] = 'ALTER';
// Affiche le tableau d'origine et la copy
echo "tree => ",var_dump($tree);
echo "copy => ",var_dump($copy); 

et le résultat

Code : Tout sélectionner

object(ArrayObject)#1 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } } object(ArrayObject)#1 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } } object(ArrayObject)#1 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } } inwhile key = one & $tree=>object(ArrayObject)#2 (1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } object(ArrayObject)#1 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } } inwhile key = two & $tree=>object(ArrayObject)#3 (1) { ["three"]=> string(4) "LEAF" } object(ArrayObject)#1 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } } before unset =>object(ArrayObject)#3 (1) { ["three"]=> string(4) "LEAF" } object(ArrayObject)#1 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } } after unset =>NULL object(ArrayObject)#1 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } } tree => object(ArrayObject)#1 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(5) "ALTER" } } } copy => object(ArrayObject)#3 (1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> string(4) "LEAF" } } }

Administrateur PHPfrance
Administrateur PHPfrance | 3088 Messages

30 juin 2007, 14:53

Je pense que tu es tombé sur un bug qui a été corrigé dans la 5.2.1, parce que je n'ai pas le même résultat quand j'exécute ton script. Ici, les références ne s'assumulent pas dans l'arbre:
(PHP 5.2.4-dev (cli) (built: Jun 27 2007 20:04:30))

Code : Tout sélectionner

inwhile key = three & $tree=>array(1) { ["one"]=> array(1) { ["two"]=> &array(1) { ["three"]=> string(8) "__LEAF__" } } } before unset =>string(8) "__LEAF__" array(1) { ["one"]=> array(1) { ["two"]=> array(1) { ["three"]=> &string(8) "__LEAF__" } } }

Mammouth du PHP | 505 Messages

30 juin 2007, 15:01

Han ! j'ai qd meme trouvé un bug alors :)

Dommage que debug_zval_dump n'indique pas l'état du flag is_ref (y a qd meme le & qd is_ref = 1 dans les tableau mais pas sur une variable)
..

Après quelque test, le bout de code avec SPL en fait- ne fonctionne pas.
$pointer ne se déplace pas vériablement dans l'arbre... Bref, cette méthode n'est pas bonne.
Peut etre avec des iterator mais je n'y crois pas trop.

Je ne vois pas trop d'autre solution que l'utilisation de référence a mon pb.

Transformer un chaine de type one/two/three/four en tableau récursif
$tab['one']['two']['three']['four'] ... Sachant que c'est un tableau que je met a jours en plusieurs fois, et que la fois d'après ca peut etre one/two/six/seven ...

Actuellement, je n'ai trouvé que 2 facons de faire. Soit par les pointers, ce qui me semblait le plus logique et le plus propre. Soit avec un eval(), mais je beurk les evals...

Suite a des tests avec la 5.2.3, c'est bien le bug #33282 qui posait problème.
Etant donné que j'effectue le parcours du tableau via des références dans une methode, il n'y a pas a faire de unset apres le while, c'est une variable local qui disparait automatiquement a la fin de la methode et la référence avec :) .
merci a tous.