Генератори · PHP Manual
Генератори дають простий спосіб реалізувати нескладні ітератори без накладних витрат і складності, які тягне за собою клас із реалізацією інтерфейсу Iterator.
Генератор зручно постачає дані циклам foreach, не вимагаючи заздалегідь будувати масив у памʼяті - масив може перевищити ліміт памʼяті або забрати чимало часу на генерацію. Замість цього використовують функцію-генератор: вона нічим не відрізняється від звичайної функції, хіба що замість одноразового return генератор може виконати yield стільки разів, скільки потрібно, щоб віддати значення для перебору. Як і в ітераторів, довільний доступ до даних неможливий.
Простий приклад - переписати функцію range як генератор. Стандартний range мусить сформувати масив з усіма значеннями і повернути його, а масив може вийти чималим: скажімо, виклик range(0, 1000000) зʼїсть добряче понад 10 МБ памʼяті.
Як альтернативу можна реалізувати генератор xrange(), якому знадобиться памʼять лише на створення обʼєкта Iterator і на внутрішнє відстеження поточного стану генератора, а це менш ніж кілобайт.
Приклад: реалізація range як генератора
<?php
function xrange($start, $limit, $step = 1) {
if ($start <= $limit) {
if ($step <= 0) {
throw new LogicException('Step must be positive');
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException('Step must be negative');
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
/*
* Зверніть увагу: і range(), і xrange() дають
* однаковий результат.
*/
echo 'Single digit odd numbers from range(): ';
foreach (range(1, 9, 2) as $number) {
echo "$number ";
}
echo "\n";
echo 'Single digit odd numbers from xrange(): ';
foreach (xrange(1, 9, 2) as $number) {
echo "$number ";
}
Результат виконання:
Single digit odd numbers from range(): 1 3 5 7 9
Single digit odd numbers from xrange(): 1 3 5 7 9
Обʼєкти Generator
Виклик функції-генератора повертає новий обʼєкт внутрішнього класу Generator. Цей обʼєкт реалізує інтерфейс Iterator майже так само, як це робив би ітератор, що рухається лише вперед, і надає методи для маніпуляцій зі станом генератора, зокрема для надсилання значень до нього та отримання значень з нього.
Синтаксис генераторів
Функція-генератор виглядає як звичайна функція, тільки замість повернення значення вона виконує yield стільки разів, скільки потрібно. Будь-яка функція, що містить yield, є функцією-генератором.
Виклик функції-генератора повертає обʼєкт, який можна перебирати. Коли ви перебираєте цей обʼєкт (наприклад, циклом foreach), PHP щоразу, коли йому потрібне значення, викликає методи ітерації обʼєкта, а потім зберігає стан генератора в момент видачі значення, щоб відновити виконання, коли знадобиться наступне значення.
Коли значень для видачі більше немає, генератор просто завершується, і код, що його викликав, продовжує роботу так, ніби в масиві закінчилися значення.
Генератор може повертати значення, яке дістають через
Generator::getReturn.
Ключове слово yield
Серцем функції-генератора є ключове слово yield. У найпростішій формі інструкція yield схожа на return, але замість того щоб зупинити виконання функції й повернути значення, yield віддає значення коду, який перебирає генератор, і ставить виконання функції-генератора на паузу.
Приклад: проста видача значень
<?php
function gen_one_to_three() {
for ($i = 1; $i <= 3; $i++) {
// Зверніть увагу: $i зберігається між викликами yield.
yield $i;
}
}
$generator = gen_one_to_three();
foreach ($generator as $value) {
echo "$value\n";
}
Результат виконання:
1
2
3
Усередині з виданими значеннями зіставляються послідовні цілочислові ключі, так само як у неасоціативному масиві.
Видача значень із ключами
PHP підтримує асоціативні масиви, і генератори тут не виняток. Крім видачі простих значень, як показано вище, ви можете одночасно видавати й ключ.
Синтаксис видачі пари ключ/значення дуже схожий на той, яким визначають асоціативний масив, як показано нижче.
Приклад: видача пари ключ/значення
<?php
/*
* На вході поля, розділені крапкою з комою; перше поле -
* це ідентифікатор, який використовується як ключ.
*/
$input = <<<'EOF'
1;PHP;Likes dollar signs
2;Python;Likes whitespace
3;Ruby;Likes blocks
EOF;
function input_parser($input) {
foreach (explode("\n", $input) as $line) {
$fields = explode(';', $line);
$id = array_shift($fields);
yield $id => $fields;
}
}
foreach (input_parser($input) as $id => $fields) {
echo "$id:\n";
echo " $fields[0]\n";
echo " $fields[1]\n";
}
Результат виконання:
1:
PHP
Likes dollar signs
2:
Python
Likes whitespace
3:
Ruby
Likes blocks
Видача значень null
yield можна викликати без аргументу, щоб видати значення null з автоматичним ключем.
Приклад: видача null
<?php
function gen_three_nulls() {
foreach (range(1, 3) as $i) {
yield;
}
}
var_dump(iterator_to_array(gen_three_nulls()));
Результат виконання:
array(3) {
[0]=>
NULL
[1]=>
NULL
[2]=>
NULL
}
Видача за посиланням
Функції-генератори можуть видавати значення як за значенням, так і за посиланням. Робиться це так само, як повернення посилань із функцій: перед іменем функції ставиться амперсанд.
Приклад: видача значень за посиланням
<?php
function &gen_reference() {
$value = 3;
while ($value > 0) {
yield $value;
}
}
/*
* Зверніть увагу: ми можемо змінювати $number всередині циклу,
* і оскільки генератор видає посилання, $value всередині
* gen_reference() теж змінюється.
*/
foreach (gen_reference() as &$number) {
echo (--$number).'... ';
}
Результат виконання:
2... 1... 0...
Делегування генератора через yield from
Делегування генератора дозволяє видавати значення з іншого генератора, обʼєкта Traversable чи масиву (array) за допомогою ключового слова yield from. Зовнішній генератор віддаватиме всі значення внутрішнього генератора, обʼєкта чи масиву, доки той не вичерпається, після чого виконання продовжиться у зовнішньому генераторі.
Якщо з yield from використано генератор, вираз yield from також поверне будь-яке значення, повернуте внутрішнім генератором.
Обережно: збереження в масив (наприклад, через
iterator_to_array)
yield fromне скидає ключі. Він зберігає ключі, повернуті обʼєктомTraversableчи масивом (array). Тому деякі значення можуть мати спільний ключ з іншимyieldабоyield from, і під час вставки в масив попередні значення з таким ключем будуть перезаписані.Типовий випадок, де це має значення, -
iterator_to_array, яка за замовчуванням повертає масив із ключами, що може дати несподіваний результат. Уiterator_to_arrayє другий параметрpreserve_keys, і якщо встановити його вfalse, зберуться всі значення, а ключі, повернутіGenerator, буде проігноровано.Приклад:
yield fromзiterator_to_array<?php function inner() { yield 1; // ключ 0 yield 2; // ключ 1 yield 3; // ключ 2 } function gen() { yield 0; // ключ 0 yield from inner(); // ключі 0-2 yield 4; // ключ 1 } // передайте false другим параметром, щоб отримати масив [0, 1, 2, 3, 4] var_dump(iterator_to_array(gen()));Результат виконання:
array(3) { [0]=> int(1) [1]=> int(4) [2]=> int(3) }
Приклад: базове використання yield from
<?php
function count_to_ten() {
yield 1;
yield 2;
yield from [3, 4];
yield from new ArrayIterator([5, 6]);
yield from seven_eight();
yield 9;
yield 10;
}
function seven_eight() {
yield 7;
yield from eight();
}
function eight() {
yield 8;
}
foreach (count_to_ten() as $num) {
echo "$num ";
}
Результат виконання:
1 2 3 4 5 6 7 8 9 10
Приклад: yield from і значення, що повертаються
<?php
function count_to_ten() {
yield 1;
yield 2;
yield from [3, 4];
yield from new ArrayIterator([5, 6]);
yield from seven_eight();
return yield from nine_ten();
}
function seven_eight() {
yield 7;
yield from eight();
}
function eight() {
yield 8;
}
function nine_ten() {
yield 9;
return 10;
}
$gen = count_to_ten();
foreach ($gen as $num) {
echo "$num ";
}
echo $gen->getReturn();
Результат виконання:
1 2 3 4 5 6 7 8 9 10
Порівняння генераторів з обʼєктами Iterator
Головна перевага генераторів - простота. Шаблонного коду доводиться писати значно менше, ніж під час реалізації класу Iterator, і код зазвичай читається краще. Наприклад, наступні функція і клас рівноцінні:
<?php
function getLinesFromFile($fileName) {
if (!$fileHandle = fopen($fileName, 'r')) {
return;
}
while (false !== $line = fgets($fileHandle)) {
yield $line;
}
fclose($fileHandle);
}
// versus...
class LineIterator implements Iterator {
protected $fileHandle;
protected $line;
protected $i;
public function __construct($fileName) {
if (!$this->fileHandle = fopen($fileName, 'r')) {
throw new RuntimeException('Couldn\'t open file "' . $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);
}
}
За цю гнучкість доводиться платити: генератори рухаються лише вперед, і після початку перебору їх не можна перемотати назад. Це також означає, що той самий генератор не можна перебрати кілька разів: доведеться створити його заново, знову викликавши функцію-генератор.
Дивіться також
Перекладаємо з офіційної документації, розділ за розділом, і не ховаємо недоперекладене. Помітили неточність у терміні чи реченні: напишіть, виправимо.