Что значит yield в PHP?

Я недавно наткнулся на этот код:

function xrange($min, $max) 
{
    for ($i = $min; $i <= $max; $i++) {
        yield $i;
    }
}

Я никогда этого не видела yield Ключевое слово перед. Пытаясь запустить код, который я получаю

Ошибка разбора: синтаксическая ошибка, неожиданный T_VARIABLE в строке x

И что это yield ключевое слово? Это даже действительный PHP? И если это так, как я могу его использовать?

9 ответов

Решение

Что такое yield?

yield Ключевое слово возвращает данные из функции генератора:

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

Что такое функция генератора?

Функция генератора - фактически более компактный и эффективный способ написать Итератор. Это позволяет вам определить функцию (ваш xrange), который будет вычислять и возвращать значения, пока вы зацикливаетесь на нем:

foreach (xrange(1, 10) as $key => $value) {
    echo "$key => $value", PHP_EOL;
}

Это создаст следующий вывод:

0 => 1
1 => 2
…
9 => 10

Вы также можете контролировать $key в foreach используя

yield $someKey => $someValue;

В функции генератора, $someKey это то, что вы хотите появиться для $key а также $someValue быть значением в $val, В примере вопроса это $i,

В чем разница с обычными функциями?

Теперь вы можете задаться вопросом, почему мы не просто используем нативный PHP range функция для достижения этого выхода. И ты прав. Вывод будет таким же. Разница в том, как мы туда попали.

Когда мы используем range PHP, выполнит его, создаст весь массив чисел в памяти и return весь этот массив к foreach цикл, который затем пройдет по нему и выведет значения. Другими словами, foreach будет работать на самом массиве. range функция и foreach только "поговорить" один раз. Думайте об этом как о получении посылки по почте. Курьер доставит вам посылку и уйдет. А затем вы разворачиваете весь пакет, вынимая все, что там есть.

Когда мы используем функцию генератора, PHP входит в функцию и выполняет ее, пока она не достигнет конца или не yield ключевое слово. Когда он встречает yield, тогда он вернет любое значение в то время во внешний цикл. Затем он возвращается в функцию генератора и продолжает с того места, где он уступил. Так как ваш xrange держит for цикл, он будет выполняться и давать до $max был достигнут Думайте об этом как foreach и генератор, играющий в пинг-понг.

Зачем мне это?

Очевидно, что генераторы могут использоваться для обхода ограничений памяти. В зависимости от вашей среды, делая range(1, 1000000) будет фатальным ваш сценарий, тогда как то же самое с генератором будет просто отлично работать. Или, как говорит Википедия:

Поскольку генераторы вычисляют свои полученные значения только по требованию, они полезны для представления последовательностей, которые было бы дорого или невозможно вычислить сразу. К ним относятся, например, бесконечные последовательности и потоки данных в реальном времени.

Генераторы также должны быть довольно быстрыми. Но имейте в виду, что когда мы говорим о быстром, мы обычно говорим в очень небольших количествах. Поэтому, прежде чем вы теперь запустите и измените весь свой код на использование генераторов, сделайте тест, чтобы увидеть, где это имеет смысл.

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

С каких пор я могу использовать yield?

Генераторы были введены в PHP 5.5. Пытаясь использовать yield до этой версии будут возникать различные ошибки синтаксического анализа, в зависимости от кода, который следует за ключевым словом. Поэтому, если вы получили ошибку разбора этого кода, обновите ваш PHP.

Источники и дальнейшее чтение:

Эта функция использует yield:

function a($items) {
    foreach ($items as $item) {
        yield $item + 1;
    }
}

почти такой же, как этот без:

function b($items) {
    $result = [];
    foreach ($items as $item) {
        $result[] = $item + 1;
    }
    return $result;
}

Только с одним отличием a() возвращает генератор и b() просто простой массив. Вы можете повторить и то и другое.

Кроме того, первый не выделяет полный массив и поэтому требует меньше памяти.

Ни в одном из приведенных выше ответов не приводится конкретный пример использования массивных массивов, заполненных нечисловыми элементами. Вот пример использования массива, созданногоexplode() в большом файле.txt (262 МБ в моем случае использования):

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

$path = './file.txt';
$content = file_get_contents($path);

foreach(explode("\n", $content) as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

Результат был:

Starting memory usage: 415160
Final memory usage: 270948256

Теперь сравните это с аналогичным скриптом, используя yield ключевое слово:

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

function x() {
    $path = './file.txt';
    $content = file_get_contents($path);
    foreach(explode("\n", $content) as $x) {
        yield $x;
    }
}

foreach(x() as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

Результатом этого сценария было:

Starting memory usage: 415152
Final memory usage: 415616

Очевидно, что экономия использования памяти была значительной (ΔMemoryUsage -----> ~ 270,5 МБ в первом примере, ~450Б во втором примере).

простой пример

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $v)
    echo $v.',';
echo '#end main#';
?>

выход

#start main# {start[1,2,3,4,5,6,7,8,9,]end} #end main#

yield Ключевое слово служит для определения "генераторов" в PHP 5.5. Хорошо, тогда что такое генератор?

С php.net:

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

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

Отсюда: генераторы = генераторы, другие функции (просто простые функции) = функции.

Итак, они полезны, когда:

  • вам нужно делать простые вещи (или простые вещи);

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

  • вам нужно генерировать большие объемы данных - экономя память;

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

  • вам нужно сгенерировать последовательность, которая зависит от промежуточных значений;

    это расширение предыдущей мысли. генераторы могут сделать вещи проще по сравнению с функциями. проверьте пример Фибоначчи и попробуйте составить последовательность без генератора. Также генераторы могут работать быстрее в этом случае, по крайней мере, из-за хранения промежуточных значений в локальных переменных;

  • вам нужно улучшить производительность.

    в некоторых случаях они могут работать быстрее, чем функции (см. предыдущее преимущество);

С yield Вы можете легко описать точки останова между несколькими задачами в одной функции. Вот и все, в этом нет ничего особенного.

$closure = function ($injected1, $injected2, ...){
    $returned = array();
    //task1 on $injected1
    $returned[] = $returned1;
//I need a breakpoint here!!!!!!!!!!!!!!!!!!!!!!!!!
    //task2 on $injected2
    $returned[] = $returned2;
    //...
    return $returned;
};
$returned = $closure($injected1, $injected2, ...);

Если task1 и task2 тесно связаны, но вам нужна точка останова между ними, чтобы сделать что-то еще:

  • свободная память между строками базы данных
  • запускать другие задачи, которые обеспечивают зависимость от следующей задачи, но которые не связаны с пониманием текущего кода
  • делать асинхронные вызовы и ждать результатов
  • и так далее...

тогда генераторы - лучшее решение, потому что вам не нужно разбивать свой код на множество замыканий или смешивать его с другим кодом, или использовать обратные вызовы и т. д. Вы просто используете yield добавить точку останова, и вы можете продолжить с этой точки останова, если вы готовы.

Добавить точку останова без генераторов:

$closure1 = function ($injected1){
    //task1 on $injected1
    return $returned1;
};
$closure2 = function ($injected2){
    //task2 on $injected2
    return $returned1;
};
//...
$returned1 = $closure1($injected1);
//breakpoint between task1 and task2
$returned2 = $closure2($injected2);
//...

Добавить точку останова с генераторами

$closure = function (){
    $injected1 = yield;
    //task1 on $injected1
    $injected2 = (yield($returned1));
    //task2 on $injected2
    $injected3 = (yield($returned2));
    //...
    yield($returnedN);
};
$generator = $closure();
$returned1 = $generator->send($injected1);
//breakpoint between task1 and task2
$returned2 = $generator->send($injected2);
//...
$returnedN = $generator->send($injectedN);

примечание: с генераторами легко ошибиться, поэтому всегда пишите модульные тесты, прежде чем применять их!note2: Использование генераторов в бесконечном цикле похоже на написание замыкания бесконечной длины...

Интересный аспект, который стоит обсудить здесь, дает ссылки. Каждый раз, когда нам нужно изменить параметр таким образом, чтобы он отражался вне функции, мы должны передавать этот параметр по ссылке. Чтобы применить это к генераторам, мы просто добавляем амперсанд & к имени генератора и к переменной, используемой в итерации:

 <?php 
 /**
 * Yields by reference.
 * @param int $from
 */
function &counter($from) {
    while ($from > 0) {
        yield $from;
    }
}

foreach (counter(100) as &$value) {
    $value--;
    echo $value . '...';
}

// Output: 99...98...97...96...95...

В приведенном выше примере показано, как изменить повторяющиеся значения в пределах foreach цикл изменяет $from переменная внутри генератора. Это потому что $from выдается по ссылке из-за амперсанда перед именем генератора. Из-за этого $value переменная в пределах foreach цикл является ссылкой на $from переменная в функции генератора.

В приведенном ниже коде показано, как использование генератора возвращает результат до завершения, в отличие от традиционного подхода без генератора, который возвращает полный массив после полной итерации. С генератором ниже значения возвращаются, когда они готовы, нет необходимости ждать полного заполнения массива:

<?php 

function sleepiterate($length) {
    for ($i=0; $i < $length; $i++) {
        sleep(2);
        yield $i;
    }
}

foreach (sleepiterate(5) as $i) {
    echo $i, PHP_EOL;
}

При реализации интерфейса PHP IteratorAggregate ключевое слово будет полезно. Ознакомьтесь с документацией, есть пара примеров использования ArrayIteratorили же yield.

Другой пример можно найти в php-ds/polyfillрепо: https://github.com/php-ds/polyfill/blob/e52796c50aac6e6cfa6a0e8182943027bacbe187/src/Traits/GenericSequence.php#L359

Идея аналогична быстрому примеру ниже:

      class Collection implements \IteratorAggregate
{
    private $array = [];

    public function push(...$values)
    {
        array_push($this->array, ...$values);
    }

    public function getIterator()
    {
        foreach ($this->array as $value) {
            yield $value;
        }
    }
}

$collection = new Collection();
$collection->push('apple', 'orange', 'banana');

foreach ($collection as $key => $value) {
    echo sprintf("[%s] => %s\n", $key, $value);
}

Выход:

      [0] => apple
[1] => orange
[2] => banana
Другие вопросы по тегам