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


232

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

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

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

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

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

Ответы:


355

Что такое 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.

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

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

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

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

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

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

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

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

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

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

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

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


1
Пожалуйста, уточните, в чем преимущества yeild, скажем, такого решения, как это: ideone.com/xgqevM
Майк

1
Ах, ну и уведомления, которые я генерировал. Да. Ну, я экспериментировал с эмулятором Generators для PHP> = 5.0.0 с помощью вспомогательного класса, и да, немного менее читабельным, но я могу использовать это в будущем. Интересная тема. Спасибо!
Майк

Не читабельность, а использование памяти! Сравните использованную память для перебора return range(1,100000000)и for ($i=0; $i<100000000; $i++) yield $i
emix

@mike да, это уже объяснялось в моем ответе. В другом примере память Майка вряд ли является проблемой, потому что он перебирает только 10 значений.
Гордон

1
@Mike Одна проблема с xrange заключается в том, что использование статических ограничений полезно, например, для вложения (например, для поиска по n-мерному многообразию или для рекурсивной быстрой сортировки с использованием генераторов). Вы не можете вкладывать циклы xrange, потому что есть только один экземпляр его счетчика. Версия Yield не страдает этой проблемой.
Шейн

43

Эта функция использует 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()просто простой массив. Вы можете повторить и то и другое.

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


2
Примечания к добавлению из официальных документов: В PHP 5 генератор не мог вернуть значение: это может привести к ошибке компиляции. Пустой оператор возврата был допустимым синтаксисом в генераторе, и он завершит работу генератора. Начиная с PHP 7.0, генератор может возвращать значения, которые можно получить с помощью Generator :: getReturn (). php.net/manual/en/language.generators.syntax.php
программист Данчук

Просто и лаконично.
Джон Миллер

24

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

<?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#

расширенный пример

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

вывод

#start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#

Итак, он возвращает без прерывания функции?
Лукас Бустаманте

22

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

С php.net:

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

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

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

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

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

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

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

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

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

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

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

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


1
Я не понял, как работают генераторы. этот класс реализует интерфейс итератора. Исходя из того, что я знаю, классы итераторов позволяют мне настроить способ итерации по объекту. например, ArrayIterator получает массив или объект, чтобы я мог изменять значения и ключи во время итерации. так что, если итераторы получают весь объект / массив, то как генератору не нужно строить весь массив в памяти ???
user3021621

7

С 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: Использование генераторов в бесконечном цикле похоже на написание замыкания бесконечной длины ...


4

Ни один из ответов выше не показывает конкретный пример использования массивных массивов, заполненных нечисловыми элементами. Вот пример использования массива, созданного 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 В во втором примере).


3

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

 <?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переменную в функции генератора.


0

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

<?php 

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

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

Таким образом, невозможно использовать yield для генерации HTML-кода в PHP? Я не знаю преимуществ в реальной среде
Джузеппе Лоди Риццини

@GiuseppeLodiRizzini, что заставляет вас так думать?
Брэд Кент
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.