Генераторы
Генераторы это обычно функции, которые возвращают объект класса Generator. Функцию-генератор легко определить по вызову ключевого слова yield. Если в функции существует это ключевое слово и эта функция была вызвана, то код в ней не выполняется. Вместо этого возвращается объект генератора. Код внутри будет выполнен только когда произойдет итерация генератора.
function gen() {
echo "One";
yield "Two";
echo "Three";
yield "Four";
echo "Five";
}
$generator = gen();
В данном случае, хотя и была вызвана функция gen(), но код в функции не выполнялся. Вместо этого переменная $generator теперь является генератором.
Сам итератор работает следующим образом. Сначала вызывается метод rewind(), а затем метод valid(), чтобы определить нужна ли вообще итерация. Затем будет вызван метод current() чтобы получить первое значение:
$generator->rewind();
if ($generator->valid()) {
echo $generator->current();
}
Вызов rewind() произведет вход в нашу функцию-генератор и выполнит весь код, пока не встретит ключевое слово yield. В этот момент он не выполняет выражение yield, только лишь код до него. В нашем случае это означает echo "One".
Так как не был достигнут еще конец генератора, то valid() возвращает TRUE, поэтому вызывается current(), который и выполнит выражение yield ("Two"), возвращая результат в наш echo, и снова код станет на паузу.
Чтобы перейти к следующей итерации вызывается next(), который имеет схожий эффект с rewind(), и возвращается снова в функцию, но на этот раз в место сразу за выражением yield, и продолжает выполнение пока снова не встретит yield. Это означает выполнение echo "Three".
Если же мы дальше продолжим итерацию, то valid() будет вызван и если вернет TRUE, то снова будет вызван current(), возвращая "Four" в echo и снова пауза.
Продолжая дальше к следущей итерации вызывается next(), он выполняет echo "Five", и тут мы встречаем конец функции, что заставляет valid() вернуть FALSE, останавливая итератор:
foreach ($generator as $value) {
echo $value;
}
Генераторы идельано подходят для операций итерации над данными. Другие примеры использования:
- чтение больших файлов
- обработка результатов от базы данных
- создание HTML таблиц
Закрытие генератора
Генератор считается закрытым когда:
- он встречает выражение return
- достигнут конец функции
- выбрашено и не поймано исключение внутри генератора
- все ссылки на генератор были удалены
Ключи
Так же можно возвращать ключи из генератора, используя нотацию массивов:
function tagsAndSlugs(array $tags) {
foreach ($tags as $value) {
$slug = createSlug($value);
yield $slug => $value;
}
}
Ссылки
Генератор так же может возвращать значение по ссылке. Это достигается добавлением & к вызову функции-генератора:
class DataModel
{
protected $data = [];
function &getIterator() {
foreach ($this->data as $key => $value) {
yield $key => $value;
}
}
}
// $dm = instanceof DataModel
foreach ($dm->getIterator() as $key => &$value) {
$value = strtoupper($value); // $dm->data is updated
}
Переиспользование генератора
Нельзя повторно использовать генератор, так же как обычные итераторы или массивы. Если вызвать rewind() у генератора после первого вызова yield, то будет выброшено исключение. Это означает, что нельзя произвести итерацию над одним и тем же генератором дважды.
Так же нельзя клонировать генераторы. Это вызовет фатальную ошибку: Fatal error: Trying to clone an uncloneable object of class Generator..
Если нужно повторно использовать генератор, то следует заново вызвать функцию, создавая новый объект генератора.
Корутины
Главное, что корутины добавляют к вышеописанной функциональности — это возможность отправлять значения генератору. Это делает из привычного монолога генератора полноценный диалог, где вызывающая сторона может также что-то сообщать генератору.
Значения передаются корутине вызовом метода send() вместо next(). Примером того, как это работает, послужит вот эта корутина:
function logger($fileName) {
$fileHandle = fopen($fileName, 'a');
while (true) {
fwrite($fileHandle, yield . "\n");
}
}
$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
$logger->send('Bar');
Здесь yield не как statement (как например return или echo), а как выражение, то есть он возвращает какое-то значение. Он возвратит то, что было послано через send(). В данном примере yield сначала возвратит "Foo", а потом "Bar".