Generator to funkcja zawierająca yield. Jej wywołanie nie wykonuje treści; zwraca obiekt Generator, po którym foreach może przejść pętlą. Każde yield $value przekazuje pętli jedną wartość i wstrzymuje funkcję, dopóki pętla nie poprosi o następną, więc wartości powstają po jednej, a nie są najpierw zbierane w tablicy.
Wynik przeplata generator i pętlę: każda wartość powstaje dopiero wtedy, gdy pętla o nią poprosi.
Generatory oszczędzają pamięć
range(1, 1000000) buduje tablicę miliona liczb całkowitych, zanim pętla się zacznie. Generator zwracający te same liczby nigdy nie trzyma więcej niż jednej z nich. Uruchom to i porównaj pamięć potrzebną każdej wersji:
Obie pętle wypisują tę samą sumę, 500000500000, ale wersja z tablicą zgłasza extra memory 16.0 MB, a generator extra memory 0.0 MB. Różnica rośnie wraz z danymi. Ta sama idea sprawia, że generatory przydają się przy dużych plikach, wynikach z bazy danych i stronach API: przetwórz jeden element, porzuć go, weź następny.
Klucze i wartości w yield
yield $key => $value ustawia klucz, który dostaje foreach ($gen as $key => $value). Bez klucza generator numeruje wartości od 0, jak listę. Częste zastosowanie to czytanie pliku linia po linii z numerami linii:
W pamięci jest naraz tylko jedna linia, więc ta sama funkcja obsłuży plik logu o rozmiarze 2 GB. Użyte tu funkcje plikowe opisuje strona fopen() i fgets().
Ciągi nieskończone i leniwe potoki
Generator może działać w nieskończonej pętli, bo wykonuje się tylko tak daleko, jak prosi konsument. Zatrzymaj go przez break albo otocz innym generatorem, który bierze kilka pierwszych wartości. Łączenie małych generatorów daje potok, w którym każdy krok jest leniwy:
fibonacci() sam nigdy się nie kończy, a jednak program się kończy: take() przestaje prosić po żądanej liczbie wartości i nic poza tym nigdy nie jest obliczane.
yield from: przekazanie do innego generatora
yield from przepuszcza każdą wartość tablicy, iteratora albo innego generatora. Tak dzielisz generator na mniejsze albo rekurencyjnie przechodzisz zagnieżdżoną strukturę:
yield from zachowuje wewnętrzne klucze, więc obie tablice zwracają klucze 0 i 1. iterator_to_array() nadpisuje wtedy a i b. Przekaż false jako drugi argument zawsze, gdy liczą się tylko wartości.
send() i getReturn()
Generator może też przyjmować wartości. $x = yield; wstrzymuje i czeka; $gen->send($value) wznawia generator z $value jako wynikiem tego yield. Generator może też przez return zwrócić wartość końcową, którą odczytasz przez getReturn() po jego zakończeniu.
Większość kodu nigdy nie potrzebuje send(); codzienne zastosowanie to leniwe czytanie przez foreach. To element, na którym opierają się biblioteki korutyn.
Po generatorze można przejść tylko raz
Generatory działają do przodu i nie da się ich przewinąć. Gdy foreach zużyje jeden, druga pętla po tym samym obiekcie rzuca wyjątek:
Rozwiązanie to wywołanie funkcji generatora od nowa przy każdym przejściu, foreach (colors() as $c), albo jednorazowe zebranie wartości przez iterator_to_array(colors(), false), gdy naprawdę potrzebujesz ich dwa razy. Jeśli robisz to często, dla tych danych właściwym wyborem była zwykła tablica.
Najczęściej zadawane pytania
Co robi yield w PHP?
yield $value przekazuje jedną wartość kodowi, który przechodzi pętlą po funkcji, i wstrzymuje funkcję dokładnie w tym miejscu. Kolejny przebieg pętli wznawia ją po yield. Każda funkcja zawierająca yield po wywołaniu zwraca obiekt Generator zamiast się wykonać.
Po co używać generatora zamiast tablicy?
Tablica trzyma w pamięci wszystkie elementy naraz; generator wytwarza po jednym elemencie. Pętla po range(1, 1000000) alokuje wszystkie milion liczb, a generator, który je zwraca przez yield, utrzymuje stałe zużycie pamięci. Generatory pozwalają też na ciągi nieskończone i przerywają pracę wcześniej, gdy pętla się kończy.
Czym jest yield from w PHP?
yield from $source przekazuje dalej każdą wartość innego generatora, tablicy lub Traversable, tak jakby bieżący generator sam je zwrócił przez yield. Jego wartością jest też wartość zwracana przez wewnętrzny generator.
Czy można przejść po generatorze dwa razy?
Nie. Generator działa tylko do przodu. Po zakończeniu foreach ponowna pętla rzuca Exception: Cannot traverse an already closed generator. Wywołaj funkcję generatora jeszcze raz, aby dostać nowy generator, albo zapisz wartości przez iterator_to_array(), jeśli potrzebujesz ich dwa razy.
Jak zamienić generator na tablicę?
iterator_to_array($gen) zbiera wszystkie wartości z zachowaniem kluczy generatora. Jeśli klucze się powtarzają, na przykład po yield from, późniejsze wartości nadpisują wcześniejsze, więc przekaż false jako drugi argument, aby dostać zwykłą listę: iterator_to_array($gen, false).