ГенераторыЗнакомство с генераторами
Генераторы — легкий способ реализации простых
итераторов
без дополнительных ресурсов или сложностей, которые связаны с написанием класса,
который реализует интерфейс Iterator.
Генератор поддерживает удобную передачу данных в циклы &foreach;
без предварительной загрузки массива в память, что иногда
вызывает превышение программой предела памяти или значительно увеличивает время обработки,
которое уходит на генерацию результата. Вместо этого пишут функцию-генератор,
которая аналогична стандартной функции,
за исключением того, что вместо возврата
единого значения с результатом работы цикла, генератор через ключевое слово &yield; умеет
выдавать результат столько раз, сколько значений требуется перебрать.
Как и итераторы, генераторы не поддерживают произвольный доступ к элементам массива.
Простой пример этого — переопределение функции
range как генератора. Стандартная функция
range генерирует массив, который состоит из значений, и
возвращает его, что приводит к генерации больших массивов: например,
вызов range(0, 1000000) займёт более 100 МБ
оперативной памяти.
В качестве альтернативы можно создать генератор xrange(),
который использует память только чтобы создать объект
Iterator и сохранить текущее состояние, что потребует
не больше 1 килобайта памяти.
Реализация функции range как генератора
= 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 ";
}
?>
]]>
&example.outputs;
Объект Generator
Когда функция-генератор вызывается, она возвращает объект встроенного
класса Generator. Этот объект реализует интерфейс
Iterator, станет однонаправленным объектом итератора
и предоставит методы, с помощью которых можно управлять его состоянием, включая
передачу в него и возвращения из него значений.
Синтаксис генераторов
Функция-генератор выглядит как обычная функция, за исключением того, что
вместо возврата значения генератор выдаёт столько значений, столько
ему необходимо.
Каждая функция с оператором &yield; — функция-генератор.
Когда вызывается генератор, он возвращает объект, который можно итерировать.
При итерации по этому объекту (например, в цикле &foreach;), PHP вызывает
методы итерации объекта каждый раз, когда ему требуется значение, а затем
сохраняет состояние генератора и при следующем вызове возвращает следующее
значение.
Когда значения в генераторе закончатся, генератор может просто выполнить возврат,
и вызывающий код продолжится так же, как если бы в массиве закончились
значения.
Генераторы умеют возвращать значения, которые можно получить
методом Generator::getReturn.
Ключевое слово yield
Сердце функции-генератора — ключевое слово yield.
В простейшей форме инструкция yield похожа на инструкцию
return, за исключением того, что вместо остановки выполнения функции и возврата,
yield отдаёт значение коду, который выполняет цикл над генератором,
и приостанавливает выполнение функции генератора.
Простой пример выдачи значений
]]>
&example.outputs;
Внутренне последовательные целочисленные ключи свяжутся с полученными значениями,
как и в случае с неассоциативным массивом.
Получение значений с ключами
PHP также поддерживает ассоциативные массивы, и генераторы — не исключение.
Помимо получения простых значений, как показывает пример, разрешается также
одновременно получить ключ.
Синтаксис получения пары ключ и значение очень похож на синтаксис определения
ассоциативных массивов, как показывает следующий пример.
Получение пар ключ/значение
$fields;
}
}
foreach (input_parser($input) as $id => $fields) {
echo "$id:\n";
echo " $fields[0]\n";
echo " $fields[1]\n";
}
?>
]]>
&example.outputs;
Получение значений null
Чтобы получить значение &null;, нужно вызвать yield без аргументов. Ключ сгенерируется
автоматически.
Получение &null;
]]>
&example.outputs;
NULL
[1]=>
NULL
[2]=>
NULL
}
]]>
Получение значения по ссылке
Функции-генераторы умеют возвращать значения как по ссылке, так и по значению. Это делается так же,
как и возврат ссылок из функций:
добавлением амперсанда (&) к имени функции.
Получение значений по ссылке
0) {
yield $value;
}
}
/* Обратите внимание, что можно изменять значение переменной $number в цикле,
* и поскольку генератор возвращает ссылку, переменная $value
* в функции gen_reference() также изменится. */
foreach (gen_reference() as &$number) {
echo (--$number).'... ';
}
?>
]]>
&example.outputs;
Делегирование генератора через yield from
Делегирование генератора позволяет получать значения
из другого генератора, объекта Traversable или массива
через ключевые слова yield from.
Внешний генератор будет возвращать значения из внутреннего генератора,
объекта или массива до тех пор, пока они не перестанут действовать, после чего
выполнение продолжится во внешнем генераторе.
Если генератор используется с ключевыми словами yield from, выражение
yield from также будет возвращать значения из
внутреннего генератора.
Сохранение в массив (например, через функцию iterator_to_array)
Ключевые слова yield from не сбрасывают ключи. Ключи, которые вернул
объект Traversable или массив, сохранятся.
Поэтому некоторые значения могут пересекаться по ключам с другими выражениями
yield или yield from, что при записи
в массив перезапишет прежние значения этим ключом.
Распространенный случай, когда это имеет значение, — функция iterator_to_array,
которая возвращает массив с ключом по умолчанию, что иногда приводит
к неожиданным результатам. У функции iterator_to_array есть второй параметр
preserve_keys, которому можно присвоить значение &false;
для генерации собственных ключей и игнорирования ключей,
которые передаются из объекта Generator.
Выражение yield from с функцией iterator_to_array
]]>
&example.outputs;
int(1)
[1]=>
int(4)
[2]=>
int(3)
}
]]>
Основы работы с выражением yield from
]]>
&example.outputs;
Выражение yield from и возвращаемые значения
getReturn();
?>
]]>
&example.outputs;
Сравнение генераторов с объектами класса Iterator
Главное преимущество генераторов — простота. Требуется написать гораздо меньше
шаблонного кода по сравнению с реализацией объекта класса
Iterator, и этот код гораздо более простой и понятный.
Например, эта функция и класс делают одно и то же.
fileHandle = fopen($fileName, 'r')) {
throw new RuntimeException('Невозможно открыть файл "' . $fileName . '"');
}
}
public function rewind()
{
fseek($this->fileHandle, 0);
$this->line = fgets($this->fileHandle);
$this->i = 0;
}
public function valid()
{
return false !== $this->line;
}
public function current()
{
return $this->line;
}
public function key()
{
return $this->i;
}
public function next()
{
if (false !== $this->line) {
$this->line = fgets($this->fileHandle);
$this->i++;
}
}
public function __destruct()
{
fclose($this->fileHandle);
}
}
?>
]]>
Однако за эту гибкость приходится платить: генераторы —
однонаправленные итераторы и их нельзя перемотать после начала
итерации. Это также означает, что один и тот же генератор нельзя повторять
несколько раз: генератор необходимо пересоздавать каждый раз, снова вызвав функцию генератора.
&reftitle.seealso;
Итераторы объектов