Знакомство с генераторами

(PHP 5 >= 5.5.0, PHP 7, PHP 8)

Генераторы — легкий способ реализации простых итераторов без дополнительных ресурсов или сложностей, которые связаны с написанием класса, который реализует интерфейс Iterator.

Генератор поддерживает удобную передачу данных в циклы foreach без предварительной загрузки массива в память, что иногда вызывает превышение программой предела памяти или значительно увеличивает время обработки, которое уходит на генерацию результата. Вместо этого пишут функцию-генератор, которая аналогична стандартной функции, за исключением того, что вместо возврата единого значения с результатом работы цикла, генератор через ключевое слово yield умеет выдавать результат столько раз, сколько значений требуется перебрать. Как и итераторы, генераторы не поддерживают произвольный доступ к элементам массива.

Простой пример этого — переопределение функции range() как генератора. Стандартная функция range() генерирует массив, который состоит из значений, и возвращает его, что приводит к генерации больших массивов: например, вызов range(0, 1000000) займёт более 100 МБ оперативной памяти.

В качестве альтернативы можно создать генератор xrange(), который использует память только чтобы создать объект Iterator и сохранить текущее состояние, что потребует не больше 1 килобайта памяти.

Пример #1 Реализация функции range() как генератора

<?php

function xrange($start, $limit, $step = 1)
{
if (
$start <= $limit) {
if (
$step <= 0) {
throw new
LogicException('Шаг должен быть положительным');
}

for (
$i = $start; $i <= $limit; $i += $step) {
yield
$i;
}
} else {
if (
$step >= 0) {
throw new
LogicException('Шаг должен быть отрицательным');
}

for (
$i = $start; $i >= $limit; $i += $step) {
yield
$i;
}
}
}

/* Обратите внимание, что и range() и xrange() дадут один и тот же вывод */

echo 'Нечётные однозначные числа с помощью range(): ';
foreach (
range(1, 9, 2) as $number) {
echo
"$number ";
}
echo
"\n";

echo
'Нечётные однозначные числа через функцию xrange(): ';
foreach (
xrange(1, 9, 2) as $number) {
echo
"$number ";
}

?>

Результат выполнения приведённого примера:

Нечётные однозначные числа через функцию range():  1 3 5 7 9
Нечётные однозначные числа через функцию xrange(): 1 3 5 7 9

Объект Generator

Когда функция-генератор вызывается, она возвращает объект встроенного класса Generator. Этот объект реализует интерфейс Iterator, станет однонаправленным объектом итератора и предоставит методы, с помощью которых можно управлять его состоянием, включая передачу в него и возвращения из него значений.

Добавить

Примечания пользователей 6 notes

up
178
bloodjazman at gmail dot com
11 years ago
for the protection from the leaking of resources see RFC https://wiki.php.net/rfc/generators#closing_a_generatorand use finnalysample codefunction getLines($file) {    $f = fopen($file, 'r');    try {        while ($line = fgets($f)) {            yield $line;        }    } finally {        fclose($f);    }}foreach (getLines("file.txt") as $n => $line) {    if ($n > 5) break;    echo $line;}
up
47
montoriusz at gmail dot com
9 years ago
Bear in mind that execution of a generator function is postponed until iteration over its result (the Generator object) begins. This might confuse one if the result of a generator is assigned to a variable instead of immediate iteration.<?php$some_state = 'initial';function gen() {    global $some_state;     echo "gen() execution start\n";    $some_state = "changed";    yield 1;    yield 2;}function peek_state() {    global $some_state;    echo "\$some_state = $some_state\n";}echo "calling gen()...\n";$result = gen();echo "gen() was called\n";peek_state();echo "iterating...\n";foreach ($result as $val) {    echo "iteration: $val\n";    peek_state();}?>If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function.<?php/**  * @return Generator  */function some_generator() {    global $some_state;    $some_state = "changed";    return gen();}?>
up
18
chung1905 at gmail dot com
5 years ago
In addition to the note of "montoriusz at gmail dot com": https://www.php.net/manual/en/language.generators.overview.php#119275"If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function."You can use Generator::rewind instead (https://www.php.net/manual/en/generator.rewind.php)Sample code:<?php/** function/generator definition **/echo "calling gen()...\n";$result = gen();$result->rewind();echo "gen() was called\n";/** iteration **/?>
up
28
info at boukeversteegh dot nl
9 years ago
Here's how to detect loop breaks, and how to handle or cleanup after an interruption.<?php    function generator()    {        $complete = false;        try {            while (($result = some_function())) {                yield $result;            }            $complete = true;        } finally {            if (!$complete) {                // cleanup when loop breaks             } else {                // cleanup when loop completes            }        }        // Do something only after loop completes    }?>
up
19
lubaev
11 years ago
Abstract test.<?php$start_time=microtime(true);$array = array();$result = '';for($count=1000000; $count--;){  $array[]=$count/2;}foreach($array as $val){  $val += 145.56;  $result .= $val;}$end_time=microtime(true);echo "time: ", bcsub($end_time, $start_time, 4), "\n";echo "memory (byte): ", memory_get_peak_usage(true), "\n";?><?php$start_time=microtime(true);$result = '';function it(){  for($count=1000000; $count--;)  {    yield $count/2;  }}foreach(it() as $val){  $val += 145.56;  $result .= $val;}$end_time=microtime(true);echo "time: ", bcsub($end_time, $start_time, 4), "\n";echo "memory (byte): ", memory_get_peak_usage(true), "\n";?>Result:----------------------------------           |  time  | memory, mb |----------------------------------| not gen  | 2.1216 | 89.25      ||---------------------------------| with gen | 6.1963 | 8.75       ||---------------------------------| diff     | < 192% | > 90%      |----------------------------------
up
14
dc at libertyskull dot com
11 years ago
Same example, different results:----------------------------------           |  time  | memory, mb |----------------------------------| not gen  | 0.7589 | 146.75     ||---------------------------------| with gen | 0.7469 | 8.75       ||---------------------------------Time in results varying from 6.5 to 7.8 on both examples.So no real drawbacks concerning processing speed.
To Top