Interface Iterator et problème de conception.

LocalStone Messages postés 514 Date d'inscription mercredi 19 mars 2003 Statut Membre Dernière intervention 1 mars 2009 - 27 nov. 2007 à 23:11
malalam Messages postés 10839 Date d'inscription lundi 24 février 2003 Statut Membre Dernière intervention 2 mars 2010 - 28 nov. 2007 à 11:32
Salut à tous,
Alors voilà ... Un nouveau post, un nouveau problème ! Mais par contre, on continue avec l'interface Iterator.
Pour un projet, j'ai du coder une classe Map qui a la donnée d'une clé de n'importe quel type (string, int, object, etc.) retourne une valeur. Le fait que la clé soit de n'importe quelle type m'empêche d'utiliser un tableu associatif. Lorsque j'ai codé la classe tout allait bien jusqu'à ce que je me rende compte d'un soucis assez problématique avec la méthode Map::removeValue($mxdKey) ... Mais je ne sais pas s'il vient de ma manière de faire ou bien de PHP.
Histoire de bien comprendre le soucis, je vais vous expliquer concrêtement le problème à l'aide d'une classe d'exemple que j'appellerais Conteneur à laquelle on impose les contraintes suivantes :
<li>on doit pouvoir la parcourir avec un foreach() ;</li><li>on doit pouvoir supprimer des élements.</li>Pour cela, je propose la solution suivante : la classe possède un attribut $Elements du type array() et implémente l'interface Iterator. De plus, la classe possède une méthode Conteneur::supprimerElement($strIndice) qui permet de supprimer l'entrée correspondant à la clé passée en paramètre. Voici donc le code produit ici.
Le soucis est le suivant, imaginons ce code ci ... Et bien ça ne marche pas du tout ... Le foreach est cassé lors de la suppression d'un element du tableau ... Et j'ai essayé un tas de truc (mais j'en parle pas pour l'instant, histoire d'induire personne en erreur et de laisser libre cours à votre imagination), mais rien n'y fait. De plus, dans l'exemple que je propose, on essaye de supprimer le premier élement du tableau, donc c'est un cas particulier qui pose problème, mais il y a un problème quelque soit la position de l'élement.
Donc voilà ... Si quelqu'un à une idée, une solution, etc. ... Je suis preneur, encore une fois !

LocalStone

10 réponses

LocalStone Messages postés 514 Date d'inscription mercredi 19 mars 2003 Statut Membre Dernière intervention 1 mars 2009
27 nov. 2007 à 23:18
Le site où j'ai posté le code les supprime au bout de 24 heures, donc je les remets là aussi, au cas où :
La classe Conteneur
<?php
/* -------------------- */
class Conteneur implements Iterator {
 
private $Elements = array();
  public function __construct($arrElements array()) {//$arrMap array()) {
$this -> Elements = $arrElements;
}
 
public function supprimerValeur($strIndice) {
unset($this -> Elements[$strIndice]);
}
 
public function recupererValeur($strIndice) {
return $this -> Elements[$strIndice];
}
 
public function key() {
return key($this -> Elements);
}
 
public function current() {
return current($this -> Elements);
}
 
public function next() {
next($this -> Elements);
}
 
public function rewind() {
reset($this -> Elements);
}
 
public function valid() {
return current($this -> Elements) !== false;
}
 
}
/* -------------------- */
?>

L'exemple
<?php
/* -------------------- */
$arrTableau = array(
'Indice n°1' => 'Salut',
'Indice n°2' => 'Les',
'Indice n°3' => 'Amis',
'Indice n°4' => 'Comment',
'Indice n°5' => 'Allez',
'Indice n°6' => 'Vous'
);
/* -------------------- */
$objConteneur = new Conteneur($arrTableau);
foreach ($objConteneur as $strIndice => $strValeur) {
if ($strIndice == 'Indice n°1') {
$objConteneur -> supprimer($strIndice);
} else {
echo'$arrTableau[' . $strIndice . '] = ' . $strValeur . ';
';
}
}
/* -------------------- */
?>

LocalStone
0
neigedhiver Messages postés 2480 Date d'inscription jeudi 30 novembre 2006 Statut Membre Dernière intervention 14 janvier 2011 19
27 nov. 2007 à 23:56
Salut,

Tu dis :
"De plus, la classe possède une méthode Conteneur::supprimerElement($strIndice)"

Dans le code de ta classe :
public function supprimerValeur($strIndice) {
unset($this -> Elements[$strIndice]);
}

Dans le code exemple :
$objConteneur -> supprimer($strIndice);

Mets-toi d'accord avec toi-même : comment tu veux l'appeler cette méthode ?


En fait, ce sur quoi tu itères, c'est un Array... Utilise simplement la classe ArrayIterator, qui permet déjà de supprimer un élément du tableau avec ArrayIterator::OffsetUnset()
A moins que ta classe n'ait besoin de quelque chose de plus complexe, mais d'après ton exemple, ça ne semble pas être le cas... Ou alors ton exemple n'est pas approprié...

Parce qu'en plus dans ton code, y'a des erreurs.
current ne doit pas retourner le tableau sur lequel tu itères (c'est ce que tu fais) mais l'élément sur lequel le pointeur est positionné.
current() doit donc faire ce que tu fais dans recupererValeur()
Sauf que si à recupererValeur tu passes en argument l'indice de l'élément à récupérer, ta classe n'est plus seulement un itérateur, mais aussi un accesseur.
C'est dommage, ArrayIterator est déjà un accesseur, puisqu'elle implémente ArrayAccess.

Donc je pense que tu te compliques la vie pour pas grand chose ;)
0
LocalStone Messages postés 514 Date d'inscription mercredi 19 mars 2003 Statut Membre Dernière intervention 1 mars 2009
28 nov. 2007 à 00:09
Exacte pour les noms de méthode ... J'ai pas trop vérifier lorsque j'ai viré les 25000 tests. On va l'appeller Conteneur::supprimerValeur() !
Ensuite, je suis bien conscient que ArrayIterator existe et c'est pourquoi j'ai précisé que ma classe d'exemple est naze. Mais dans le problème que j'ai est un peu plus compliqué, et j'aimerais éviter d'utiliser ArrayIterator, même si - au final - cette classe d'exemple risque d'être vraiment semblable a celle de la SPL. Pour être consi, c'est à cause du fait que la clé d'un iterateur ne peut pas être un objet. Dans la classe que je poste, il n'y a pas cette gestion des objets en temps que clé puisqu'ellle sert vraiment d'exemple, mais une fois ce problème résolu, j'ajouterais la possibilité.
Bref, en effet, la classe est à la fois un iterateur, mais aussi un accesseur ...
Au niveau de l'iterateur, je vois pas trop le soucis. En fait, parcourir mon objet revient au final à parcourir le tableau d'élement ... Donc je pense que les méthodes sont appropriées ... Non ?
LocalStone
0
malalam Messages postés 10839 Date d'inscription lundi 24 février 2003 Statut Membre Dernière intervention 2 mars 2010 25
28 nov. 2007 à 00:32
Hello,

ce n'est pas la clef d'un itérateur qui ne peut pas être un objet : c'est la clef d'un tableau. Nuance. Tu auras donc exactement le même problème avec ton code, puisque tu utilises...un tableau.
Si tu veux vraiment aller vers là -je ne sais pas si c'est possible, et il est un peu tard pour que j'essaye là, même à la va vite-, tu dois implémenter Iterator et ArrayAccess. Ce sont des interfaces. Et gérer ton "tableau" différemment, sans utiliser un vrai tableau.  Il y a plein de possibilité en fait, mais il faut tester la plus efficace (et celle qui marche lol).
0

Vous n’avez pas trouvé la réponse que vous recherchez ?

Posez votre question
LocalStone Messages postés 514 Date d'inscription mercredi 19 mars 2003 Statut Membre Dernière intervention 1 mars 2009
28 nov. 2007 à 00:40
Ralalala ... Bon, vous ne me laissez pas le choix. En fait, le problème que je présente là, je l'ai rencontré lors du codage de ces classes :
<?php
/* -------------------- */
   class Map implements Iterator, Countable {
  
      private $Keys = array();
     
      private $Values = array();
     
      public function put($mxdKey, $mxdValue) {
         if (!$this -> containsKey($mxdKey)) {
            array_push($this -> Keys, $mxdKey);
            array_push($this -> Values, $mxdValue);
         } else {
            $this -> Values[$this -> getKeyIndex($mxdKey)] = $mxdValue;
         }
      }
     
      private function getKeyIndex($mxdKey) {
         $intIndex = array_search($mxdKey, $this -> Keys);
         return ($intIndex === false) ? -1 : $intIndex;
      }
     
      public function containsKey($mxdKey) {
         return in_array($mxdKey, $this -> Keys);
      }
     
      public function containsValue($mxdValue) {
         return in_array($mxdValue, $this -> Values);
      }
     
      public function getValue($mxdKey) {
         $intIndex = $this -> getKeyIndex($mxdKey);
         return ($intIndex < 0) ? null : $this -> Values[$intIndex];
      }
     
      public function removeValue($mxdKey) {
         //echo 'Map::removeValue(' . $mxdKey . ')';
         $intKeyIndex = $this -> getKeyIndex($mxdKey);
         if ($intKeyIndex != -1) {
            unset($this -> Keys[$intKeyIndex]);
            //prev($this -> Keys);
            //$this -> Keys = array_values($this -> Keys);
            unset($this -> Values[$intKeyIndex]);
            //prev($this -> Values);
            //$this -> Values = array_values($this -> Values);
         }
      }
     
      private function getEntryByIndex($intIndex) {
         //echo 'Map::getEntryByIndex(' . (string)$intIndex . ') == ' . new MapEntry($this -> Keys[$intIndex], $this -> Values[$intIndex], $this) . '
';
         return new MapEntry($this -> Keys[$intIndex], $this -> Values[$intIndex], $this);
      }
     
      public function current() {
         $objEntry = $this -> getEntryByIndex($this -> key());
         //echo 'Map::current() == ' . $objEntry . '
';
         return $objEntry;//$this -> getEntryByIndex($this -> key());
      }
     
      public function key() {
         return key($this -> Values);
      }
     
      public function next() {
         //next($this -> Keys);
         next($this -> Values);
      }
     
      public function rewind() {
         //reset($this -> Keys);
         reset($this -> Values);
      }
     
      public function valid() {
         return current($this -> Values) !== false;
      }
     
      public function __toString() {
         $arrEntries = $this -> getEntries();
         return "{\n\t" . implode(", \n\t", $arrEntries) . "\n}";
      }
     
      public function getEntries() {
         $arrEntries = array();
         foreach ($this as $objEntry) {
            array_push($arrEntries, $objEntry);
         }
         return $arrEntries;
      }
     
      public function count() {
         return count($this -> Values);
      }
     
      public function getKeys() {
         return new Set(
            array_values(
               $this -> Keys
            )
         );
      }
     
      public function getValues() {
         return new Set(
            array_values(
               $this -> Values
            )
         );
      }
     
   }
/* -------------------- */
?>
Je voulais éviter de les poster parce que le code était trop long ... Mais le problème est le même, lors de la suppression d'un élement, lors d'un foreach, le parcour part en freestyle, et ça, bah ça va pas du tout ...
Bon, je vais me coucher aussi ... Trop prise de tête ! Mais j'espère que l'on pourra continuer l'échange dès demain !
LocalStone
0
malalam Messages postés 10839 Date d'inscription lundi 24 février 2003 Statut Membre Dernière intervention 2 mars 2010 25
28 nov. 2007 à 01:24
Hello,

avec le foreach() tu ne pourras sans doute jamais : l'offset (foreach($array as $offset => $value) ne peut pas être un objet, à priori.
Mais tu peux quand même, foreach() mis à part (mais je peux me tromper sur ce point hein) simuler largement un tableau.
J'ai repris ton principe en le simplifiant un peu, avec des exemples (ouais finalement  je ne me suis pas couché de suite donc j'ai décidé de te montrer un peu ;-) ) :
<?php

class Map implements Iterator, ArrayAccess, Countable {
    protected $aKeys = array();
    protected $aValues = array();
    protected $iPos;
   
    public function __construct() {
        $this->rewind();   
    }
   
    public function append($offset, $value) {
        $this->aKeys[] = $offset;
        $this->aValues[] = $value;
        return true;
    }
   
    public function offsetExists($offset) {
        return in_array($mKey, $this->aKeys);
    }
   
    public function offsetGet($offset) {        if(false !($mFound array_search($offset, $this->aKeys))) {
            return $this->aValues[$mFound];
        }
        return false;
    }
   
    public function offsetSet($offset, $value) {
        $this->aKeys[] = $offset;
        $this->aValues[] = $value;
        return true;
    }
   
    public function offsetUnset($offset) {        if(false !($mFound array_search($offset, $this->aKeys))) {
            unset($this->aValues[$mFound]);
            unset($this->aKeys[$mFound]);
            return true;
        }
        return false;
    }
   
    public function current() {
        return $this->aValues[$this->iPos];
    }
   
    public function key() {
        return $this->aKeys[$this->iPos];
    }
   
    public function next() {
        $this->iPos ++;
    }
   
    public function rewind() {
        $this->iPos = 0;
    }
   
    public function valid() {
        return isset($this->aKeys[$this->iPos]);
    }
    public function count() {
        return count($this->aKeys);
    }
}

class A {
    public function say() {
        return 'I am a A
';
    }
   
    public function __toString() {
        return $this->say();
    }
}

class B {
    public function say() {
        return 'I am a B
';
    }
    public function __toString() {
        return $this->say();
    }
}

$map = new Map;
$Ak = new A;
$Bv = new B;
$Av = new A;
$Bk = new B;
$map->append($Ak, $Bv);
$map->append($Bk, $Av);
echo $map[$Ak];
echo '<hr />';
while($map->valid()) {
    echo $map->key()->say();
    echo $map->current()->say();
    $map->next();
}
?>
0
neigedhiver Messages postés 2480 Date d'inscription jeudi 30 novembre 2006 Statut Membre Dernière intervention 14 janvier 2011 19
28 nov. 2007 à 01:31
@Malalam : arrête-moi (si tu peux... non c'est un film ça) si je me trompe, mais l'idée ne serait-elle pas de wrapper l'objet à itérer avec une classe qui permet de simuler un ArrayAccess ?
0
malalam Messages postés 10839 Date d'inscription lundi 24 février 2003 Statut Membre Dernière intervention 2 mars 2010 25
28 nov. 2007 à 01:42
C'était mon idée première, mais là j'ai calqué son code.
D'ailleurs j'ai oublié un truc dans mon code, essentiel. C'est peut-être le problème rencontré par LocalStone d'ailleurs : quand on supprime une valeur, il FAUT réindexer les 2 tableaux (et de la même manière), parce que sinon on a un trou, et on ne peut plus itérer correctement, forcément, je viens de voir que j'avais oublié ça en relisant mon code. Mais ce n'est pas sorcier, il suffit d'une petite méthode dédiée, pour faire ça.
0
LocalStone Messages postés 514 Date d'inscription mercredi 19 mars 2003 Statut Membre Dernière intervention 1 mars 2009
28 nov. 2007 à 10:55
Pour les offset objet, j'ai abandonné l'idée. Du coup, à la place, le Map::current() retourne une instance de la classe MapEntry qui contient 2 méthodes : MapEntry::getKey() et MapEntry::getValue() qui font ... Bah je vous laisse deviner :P
Ok ... Donc au final, si j'ai bien compris, il ne faut pas que j'utilise les pointeurs interne d'un tableau pour itérer la map, mais que j'utilise un attribut spécial qui stocke l'état d'itération (l'équivalent de ton $iPos, Malalam). C'est bien ça ?

LocalStone
0
malalam Messages postés 10839 Date d'inscription lundi 24 février 2003 Statut Membre Dernière intervention 2 mars 2010 25
28 nov. 2007 à 11:32
Tu remarqueras que mes offsets sont des objets et que cela marche (sauf avec foreach donc).
mon iPos permet simplement de ne pas déplacer ton pointeur et donne un accès plus rapide et plus direct.
0
Rejoignez-nous