Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [SPL] tworzenie iteratora
Forum PHP.pl > Forum > PHP > Object-oriented programming
DeyV
Odkąd poznałem możliwości SPL jestem pod dużym wrażeniem możliwości podsuwanych tam pomysłow, i coraz częściej staram się z nich korzystać.

Zacząłem stosować iteratory, i rzeczywiście - okazały się w wielu przypadkach bardzo przydatne, zmiejszając ilość (chyba winksmiley.jpg ) pamięci, którą musi zaalokować pamięć, i przyspieszając jego działanie w wielu przypadkach (mniejsza ilość pętli)

Zastanawiam się jednak, czy istnieje jakiś prosty sposób na tworzenie iteratoró w nieco bardziej złożonych przypadkach.

Zacznijmy od jakiegoś prostego przypadku. Np. mamy 2 tablice, a potrzebna jest nam jedna, w której znajdują się wszystkie kombinacje elementów z tych 2 powyższych (czyli każdy z każdym).
Normalnie rozwiązanie bardzo proste.
Dwa foreach, jeden drugim, i generowanie tablicy wynikowej.
  1. <?php
  2.  
  3. $aOwoce = array( 'jabłko', 'banan', 'wiśnia' );
  4. $aKolory = array( 'niebieski', 'czerwony' );
  5. $aWynik = array();
  6. foreach( $aOwoce as $sOwoc ){
  7.  foreach( $aKolory as $sKolor ){
  8. $aWynik[] = $sKolor . ' '. $sOwoc ;
  9. }
  10. }
  11. ?>


Jednak w ten sposób przechowujemy całą tą tablicę zupełnie niepotrzebnie, bo tak naprawdę do dalszego działania programu będziemy potrzebowali 1, kolejny jej element w danym momencie....
Oczywiście - nawet na chłopski rozum można napisać odpowiedni iterator, wymaga jednak całkiem złożonego algorytmu sprawdzania kolejnych elementów i ich zwracania.
A problem wydaje mi się na tyle standardowy, że powinny być jakieś "standardowe" rozwiązania - chyba że mam po prostu zaćmę i czegoś oczywistego nie udało mi sie dotychczas zauważyć... winksmiley.jpg

// ps. i nie próbujcie mi odpowiedzieć, że są jakieś standardowe funkcje do łączenia tablic, bo normalnie ... zamorduję biggrin.gif
bela
Trochę to zagmatwane, ale czy chcesz połączyć 2 tabele ? A czy przypadkiem nie można wykorzystać array_merge" title="Zobacz w manualu PHP" target="_manual ?
Vengeance
hmm... odpowiednie interfejsy deklarujesz i juz na obiekcie bedacym iteratorem mozes stosowac funkcje array_*
hawk
Standardowego rozwiązania nie będzie raczej, bo ktoś musiałby po prostu napisać odpowiedni iterator i tyle. A samo napisanie takiego iteratora jest proste. Przyjmuje w konstruktorze 2 argumenty (tablice), dla każdej robi count() i zapamiętuje, ma 2 liczniki pozycji i inkrementuje tak żeby przelecieć wszystkie kombinacje. Jaki tam złożony algorytm...

A wszystkie pomysły z array_* są bez sensu bo właśnie nie chcemy łączyć tych tablic i po to używamy iteratora, prawda?
DeyV
oczywiście, hawk.
Mimo to miałem nadzieję, że żucicsz jakimś ciekawym linkiem o tej tematyce.
Ale fakt - faktem. Po napisaniu nie wygląda to już nawet tak groźnie, i choć ciągle się zastanawiam, czy napisane jest optymalnie (jakoś dużo tych warunków mi się zrobiło :/ ) to jednak działa w pełni zgodnie z założeniami.

Dla wątpiących: winksmiley.jpg

  1. <?php
  2.  
  3. /**
  4.  * @date 9.02.2005
  5.  */
  6. class GenerateNames implements Iterator
  7. {
  8.  
  9. private $aFirst, $aSecond = array();
  10. private $iFirstCount, $iSecondCout = 0;
  11. private $iFirstIt, $iSecondIt = 0;
  12.  
  13.  
  14. function __construct( $aFirst, $aSecond ){
  15. $this->aFirst = $aFirst;
  16. $this->aSecond = $aSecond;
  17.  
  18. $this->iFirstCount = count( $this->aFirst );
  19. $this->iSecondCout = count( $this->aSecond );
  20.  
  21. }
  22.  
  23. /** Rewind the Iterator to the first element.
  24.  */
  25. function rewind() {
  26. $this->iFirstIt = $this->iSecondIt = 0;
  27. }
  28.  
  29. /** Return the current element.
  30.  */
  31. function current() {
  32. return $this->aFirst[ $this->iFirstIt ] .' '. $this->aSecond[ $this->iSecondIt ];
  33. }
  34.  
  35. /** Return the key of the current element.
  36.  */
  37. function key() {
  38. return $this->iFirstIt .'-'. $this->iSecondIt;
  39. }
  40.  
  41. /** Move forward to next element.
  42.  */
  43. function next(){
  44. if( $this->iSecondIt < $this->iSecondCout -) {
  45. ++$this->iSecondIt;
  46. }
  47. else{
  48. $this->iSecondIt = 0;
  49. ++$this->iFirstIt;
  50. }
  51. }
  52.  
  53. /** sprawdza czy nie przekroczyliśmy jeszcze zakresu
  54.  * @return bool
  55.  */
  56. function valid() {
  57. return $this->iFirstCount > $this->iFirstIt && $this->iSecondCout > $this->iSecondIt ;
  58. }
  59. }
  60.  
  61.  
  62. $Element = new GenerateNames( 
  63. array( 'jabłko', 'wiśnia', 'banan' ),
  64. array( 'czerwone', 'bordowa', 'żółty' ) );
  65.  
  66. foreach( $Element as $iKey => $sWord ) {
  67. echo $iKey . ' => ' . $sWord . '<br />';
  68. }
  69.  
  70.  
  71. ?>


mile widziane oczywiście propozycje poprawy tego kodu...

//ps. a jeśli ktoś się zastanawia, czy jest sens tworzenia taiej ilości kodu, zamiast zaprezentowanych powyżej dwóch pętli - niech wyobrazi sobie, że ten iterator nie pracuje na 2 tylko na np. 4 tablicach. I policzy - jakiej wielkości byłaby tablica wynikowa....
hawk
Kod jest dobry. Nic dodać nic ująć.

Taki przykład z dwiema tablicami faktycznie może wydawać się przerośnięty, ale tak już jest z przykładami. Wystarczy wymyśleć jakieś bardziej zakręcone kryteria łączenia tablic i już proporcje się wyrównują.

Dla lepszej ilustracji, podam iterator, który kiedyś wymyśliłem. Co prawda w Javie, ale to bez znaczenia. Niech ktoś to spróbuje zrobić kodem proceduralnym winksmiley.jpg.

1) Jest sobie drzewko w pamięci. Robimy prosty (?) iterator depth-first. No, taki prosty to on nie jest, ale wystarczy poszukać kodu - standard.

2) Robimy interfejs Chooser...
Kod
public interface Chooser {
  public boolean choose(TreeElement te);
}

...który po prostu dostaje element i mówi czy mu się podoba, czy nie.

3) Robimy ChoosingIterator...
Kod
class ChoosingIterator implements Iterator {
  public ChoosingIterator(Iterator i, Chooser c) {
    //...
  }
  //...
}

... który filtruje output zwykłego iteratora drzewkowego wykorzystując Chooser.

4) Skutek? Zrobiliśmy z tego w programie operującym na wielkich drzewach rozszerzalny system wyszukiwania. Piszesz jedną klasę implementującą Chooser, system wpina ci to w menu i już masz najgłupsze nawet kryteria wyszukiwania z Find / Find next / przewijaniem na początek dokumentu itd.

Wyszło off-topic, ale to tak w zastępstwie linka, którego dopominał się DeyV. biggrin.gif. Morał opowieści: iteratory rulez.
DeyV
dla zainteresowanych dodam, żę bardzo podobny to tego, co podał Hawk, przykład obsługi drzew w oparciu o iterator jest proponowany również przez SPL.

Zainteresowanych odsłym do kodu Iteratora struktury katalogów, w którym bardzo łatwo można zobaczyć sposó pisania filtrów, ograniczających wyświetlanie informacji. Np. usuwanie . i .. lub wszystkich folderów zaczynających sie na aa smile.gif

Jedyne co w tym wszystkim odrobinę boli, to to, że na potrzeby drzewka tworzy się wiele zagłębiających się obiektów, czyli odpowiednik bardzo nielubianego przez wielu w pisaniu strukturalnym, kod rekurencyjny.
hawk
Zawsze możesz sobie napisać iterator drzewek/katalogów, który bazuje na iteracji, nie na rekurencji.

A skoro wszystko i tak bazuje na interfejsach, możesz sobie podpiąć swój iterator w miejsce RecursiveDirectoryIterator, i reszta kodu tego nie zauważy. Chociaż DirectoryFilterDots niestety wymaga RecursiveIterator. Tutaj IMHO Boerger źle to zrobił, bo DirectoryIterator nie implementuje RecursiveIterator. Ale pewnie miał swoje powody.
antao
Hello World...

przeprowadzając testy (w pętli : x10 000) wyszło że:

-> system z iteracją: śr. czas wykonania jednej pętli
  1. <?php
  2. foreach( $Element as $iKey => $sWord ) {
  3. echo $iKey . ' => ' . $sWord . '<br />';
  4. }
  5. ?>
wynosi 0,0005207s (pamięć >32MB)

-> pętla
  1. <?php
  2. foreach( $aOwoce as $sWord => $sOwoc ){
  3.  foreach( $aKolory as $iKey => $sKolor ){
  4.  echo $sWord .'=>'. $iKey .' '. $sKolor .' '. $sOwoc .'<br />';
  5. }
  6. }
  7. ?>
śr. czas: 0,0002610s (pamięć >31MB)

tak więc różnica w szybkości wynosi 199,5%... ohmy.gif

"wąskie gardło" ?
  1. <?php
  2. $this->aFirst = $aFirst;
  3. $this->aSecond = $aSecond;
  4. ?>

z 2 robią się 4... smile.gif

Cytat
//ps. a jeśli ktoś się zastanawia, czy jest sens tworzenia taiej ilości kodu, zamiast zaprezentowanych powyżej dwóch pętli - niech wyobrazi sobie, że ten iterator nie pracuje na 2 tylko na np. 4 tablicach. I policzy - jakiej wielkości byłaby tablica wynikowa....

z tego co widzę, klasa GenerateNames jest przystosowana do obsługi tylko 2 tablic, czyli żeby obsłużyć np. 4 tablice trzeba klasę znacznie rozbudować ?
ennics
Cytat
// ps. i nie próbujcie mi odpowiedzieć, że są jakieś standardowe funkcje do łączenia tablic, bo normalnie ... zamorduję 

array_combine biggrin.gif
a iteracja to ciekawy temat, całkiem niedawno skróciłem tym sposobem
dużą część kodu, w połączeniu z wbudowanymi funkcjami operującymi na tablicach można dużo zdziałać.
dr_bonzo
@ennics: Ale array_combine nie utworzy tego co chcial osiagnac.
@DeyV: moj pierwszy Iterator dziala podobnie jak twoj 'GenerateNames', z tym ze u ciebie beda problemu z tablicami indeksowanymi inaczej niz intami poczawszy od zera (dlatego ja wykonuje przeindeksowanie).

Moje iteratory:

Drugi 'Kombinacje_Tablic_2' uzywa funkcji next(), current(), reset() itd, sa one wolniejsze i uniemozliwiaja uzywanie elementow pustych ( 0, "" ).

Pierwszy 'Kombinacje_Tablic' przeindeksowuje tablice tak zeby byly indeksowane integerami (szybszy dostep do danych, kontrola indeksow za pomoca liczb -- zapamietuje ilosc elementow i indeks aktualnego , brak bugu z pustymi elelmentami)

Dla 1000 powtorzen (samo przelecenie tablic, bez printowania indeksow i elementow):
- 2xzagniezdzony foreach: 0.25s
- pierwszy iterator: 0.89s
- drugi iterator: 1.09s

Wnioski: foreach najszybsze (zaraz sprawdze dla wiekszej ilosci tablic), przeindeksowanie tablic przyspiesza iteracje.

  1. <?php
  2. class Kombinacje_Tablic implements Iterator
  3. {
  4. private $aArray_A = array();
  5. private $aArray_B = array();
  6.  
  7. private $iCurrent_A = 0;
  8. private $iCurrent_B = 0;
  9.  
  10. private $iCount_A = 0;
  11. private $iCount_B = 0;
  12.  
  13. public function __construct( $aArray_A, $aArray_B )
  14. {
  15. // arrays will be indexed with integers
  16. foreach ( $aArray_A as $key => $value )
  17. {
  18. $this->aArray_A[] = $value;
  19. }
  20.  
  21. foreach ( $aArray_B as $key => $value )
  22. {
  23. $this->aArray_B[] = $value;
  24. }
  25.  
  26. $this->iCount_A = count( $this->aArray_A );
  27. $this->iCount_B = count( $this->aArray_B );
  28.  
  29. $this->iCurrent_A = 0;
  30. $this->iCurrent_B = 0;
  31. }
  32.  
  33. public function rewind()
  34. {
  35. $this->iCurrent_A = 0;
  36. $this->iCurrent_B = 0;
  37. }
  38.  
  39. public function key()
  40. {
  41. return ( $this->iCurrent_A . &#092;"x\" . $this->iCurrent_B );
  42. }
  43.  
  44. public function current()
  45. {
  46. return ( $this->aArray_A[ $this->iCurrent_A ] . &#092;"::\" . $this->aArray_B[ $this->iCurrent_B ] );
  47. }
  48.  
  49. public function next()
  50. {
  51. if ( $this->iCurrent_B < $this->iCount_B - 1 )
  52. {
  53. $this->iCurrent_B++;
  54. }
  55. else 
  56. {
  57. $this->iCurrent_A++;
  58. $this->iCurrent_B = 0;
  59. }
  60. }
  61.  
  62. public function valid()
  63. {
  64. return ( ( $this->iCurrent_A < $this->iCount_A ) && ( $this->iCurrent_B < $this->iCount_B ) );
  65. }
  66. }
  67.  
  68.  
  69. /*****************************************************************/
  70. /*
  71. current(), next(), itd.
  72.  
  73. If the array contains empty elements (0 or \"\", the empty string) then this function 
  74. will return FALSE for these elements as well. This makes it impossible to d
  75. termine 
  76. if you are really at the end of the list in such an array using current(). To 
  77. properly traverse an array that may contain empty elements, use the each() function.
  78. */
  79. class Kombinacje_Tablic_2 implements Iterator
  80. {
  81. private $aArray_A = array();
  82. private $aArray_B = array();
  83.  
  84. public function __construct( $aArray_A, $aArray_B )
  85. {
  86. $this->aArray_A = $aArray_A;
  87. $this->aArray_B = $aArray_B;
  88.  
  89. reset( $this->aArray_A );
  90. reset( $this->aArray_B );
  91. }
  92.  
  93. public function rewind()
  94. {
  95. reset( $this->aArray_A );
  96. reset( $this->aArray_B );
  97. }
  98.  
  99. public function key()
  100. {
  101. return ( key( $this->aArray_A ) . &#092;"x\" . key( $this->aArray_B ) );
  102. }
  103.  
  104. public function current()
  105. {
  106. return ( current( $this->aArray_A ) . &#092;"::\" . current( $this->aArray_B ) );
  107. }
  108.  
  109. public function next()
  110. {
  111. if ( next( $this->aArray_B ) === FALSE )
  112. {
  113. next( $this->aArray_A );
  114. reset( $this->aArray_B );
  115. }
  116. }
  117.  
  118. public function valid()
  119. {
  120. return ( ( current( $this->aArray_A ) !== FALSE ) && ( current( $this->aArray_B ) !== FALSE ) );
  121. }
  122. }
  123.  
  124. /*****************************************************************/
  125.  
  126. $aKolory = array( 'niebieski', 'czerwony' );
  127. $aOwoce = array( 'jabłko', 'baban', 'wiśnia' );
  128.  
  129. $dane = new Kombinacje_Tablic( $aKolory, $aOwoce );
  130. //$dane = new Kombinacje_Tablic_2( $aKolory, $aOwoce );
  131. foreach ( $dane as $key => $value )
  132. {
  133. print( &#092;"[$key] ==&gt; $value<br />\" );
  134. }
  135. ?>
DeyV
antao - ciekawy test, jednak nie do końca odpowiedni.
Zobacz, że w moim przykładzie (z 2 pętlami) tworzona była 3 tablica - do wykorzystania później.
Dopiero tą tablicę można by potraktować foreach, i dopiero to byłby dokładny odpowiednik kodu z iteratorem.
A wtedy zużycie pamięci się zwiększy conajmnie 4 krotnie. (jak sądzę winksmiley.jpg )

Cytat
"wąskie gardło" ?
...
z 2 robią się 4... smile.gif


Jednak nie. php ma bardzo ciekawą technologię kopiowania tablic on edit.
Oznacza to, że tak długo, jak któraś z tych tablic nie zostanie wyedytowana, tak dlugo w pamięci przechowywana jest tylko 1 jej instancja, a my odwołujemy ię niejako do jej referencji.
Dopiero podczas wprowadzania zmian w którejś z nich - tworzą się osobne kopie.
Jeśli masz php postawione na linuksie i xdebug - polecam przeprowadzenie dokładnego testu - ja widziałem kiedyś taki, i byłem naprawdę zaskoczony tym, jak mało pamięci zostało zużyte na skopiowanie tablicy.

Cytat
z tego co widzę, klasa GenerateNames jest przystosowana do obsługi tylko 2 tablic, czyli żeby obsłużyć np. 4 tablice trzeba klasę znacznie rozbudować ?

Tak naprawdę - wystarczły dodać liczniki dla każdej z tych tablic, zmierzyć ich wielkość, i odpowiednio rozbudować warunek.

hawk - temat - rzeka - cieszę się jednak, że udało nam sie tu zainteresować nim kilka kolejnych osób.
dr_bonzo
At last!

  1. <pre>
  2. <?php
  3. /**
  4.  *@author dr_bonzo
  5.  */
  6.  
  7. class Kombinacje_Tablic implements Iterator
  8. {
  9. // wczytane tablice
  10. private $aArrays;
  11.  
  12. private $iNumber_of_arrays = 0;
  13.  
  14. // ilosci elementow w kazdej z tablic
  15. private $aCount_array = array();
  16.  
  17. // aktualne indeksy tablic
  18. private $aCurrent_indexes = array();
  19.  
  20. public function __construct( $aArrays )
  21. {
  22. // przeindeksowanie
  23. $i = 0;
  24.  
  25. foreach ( $aArrays as $aArray )
  26. {
  27. foreach ( $aArray as $key => $value )
  28. {
  29. $this->aArrays[ $i ][] = $value;
  30. }
  31.  
  32. // zlicz wielkosc tablic
  33. $this->aCount_array[ $i ] = count( $aArray );
  34. // print( \"[$i]:: {$this->aCount_array[ $i ]}<br />\" );
  35.  
  36. // i wyzeruj counterki
  37. $this->aCurrent_indexes[ $i ] = 0;
  38. $i++;
  39. }
  40.  
  41. $this->iNumber_of_arrays = $i;
  42. }
  43.  
  44. public function rewind()
  45. {
  46. for ( $i = 0; $i < $this->iNumber_of_arrays; $i++ )
  47. {
  48. $this->aCurrent_indexes[ $i ] = 0;
  49. }
  50. }
  51.  
  52. public function key()
  53. {
  54. $sKey = '';
  55.  
  56. for ( $i = 0; $i < $this->iNumber_of_arrays; $i++ )
  57. {
  58. $sKey .= 'x' . $this->aCurrent_indexes[ $i ];
  59. }
  60.  
  61. return substr( $sKey, 1 );
  62. }
  63.  
  64. public function current()
  65. {
  66. $sCurrent = '';
  67.  
  68. for ( $i = 0; $i < $this->iNumber_of_arrays; $i++ )
  69. {
  70. $sCurrent .= ':' . $this->aArrays[ $i ][ $this->aCurrent_indexes[ $i ] ];
  71. }
  72.  
  73. return substr( $sCurrent, 1 );
  74. }
  75.  
  76. public function next()
  77. {
  78. // print( \"next()<br />\" );
  79. $i = $this->iNumber_of_arrays - 1;
  80.  
  81. if ( $this->aCurrent_indexes[ $i ] < $this->aCount_array[ $i ] )
  82. {
  83. $this->aCurrent_indexes[ $i ]++;
  84. }
  85.  
  86. while ( ( $this->aCurrent_indexes[ $i ] >= $this->aCount_array[ $i ] ) && ( $i > 0 ) )
  87. {
  88. $this->aCurrent_indexes[ $i ] = 0;
  89. $this->aCurrent_indexes[ $i - 1 ]++;
  90. $i--;
  91. }
  92. }
  93.  
  94. public function valid()
  95. {
  96. // print( \"validating<br />\" );
  97.  
  98. if ( $this->iNumber_of_arrays === 0 )
  99. {
  100. return FALSE;
  101. }
  102.  
  103. if ( $this->aCurrent_indexes[ 0 ] === $this->aCount_array[ 0 ] )
  104. {
  105. return FALSE;
  106. }
  107. else 
  108. {
  109. return TRUE;
  110. }
  111. }
  112. }
  113.  
  114. /*****************************************************************/
  115.  
  116. $aKolory = array( 'niebieski', 'czerwony' );
  117. $aOwoce = array( 'jabłko', 'banan', 'wiśnia' );
  118. $aSome_array_1 = array( 'a1fas', 'a25se5', 'a3da5' );
  119. $aSome_array_2 = array( 'b1adf', 'b2aeuj', 'b333' );
  120. $aSome_array_3 = array( 'c1fas', 'c2cse5', 'c33' );
  121.  
  122. /*
  123. $dane = new Kombinacje_Tablic( array( $aKolory, $aOwoce, $aSome_array_1, $aSome_array_2, $aSome_array_3 ) );
  124. $dane = new Kombinacje_Tablic( array( $aKolory, $aOwoce, $aSome_array_1, $aSome_array_2 ) );
  125. $dane = new Kombinacje_Tablic( array( $aKolory, $aOwoce, $aSome_array_1 ) );
  126. $dane = new Kombinacje_Tablic( array( $aKolory, $aOwoce ) );
  127. $dane = new Kombinacje_Tablic( array( $aKolory ) );
  128. */
  129. $dane = new Kombinacje_Tablic( array( $aKolory, $aOwoce, $aSome_array_1, $aSome_array_2, $aSome_array_3 ) );
  130.  
  131. foreach ( $dane as $key => $value )
  132. {
  133. print( &#092;"[$key] ==&gt; $value<br />\" );
  134. }
  135. ?>
  136. </pre>


Dla tych 5ciu tablic: 0.08 s (@ PII 350), ktos chce porownac foreach'e biggrin.gif?

Oczywiscie dziala prawidlowo dla 0, 1ej i dwoch tablic.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.