Що означає врожайність у PHP?


232

Нещодавно я наткнувся на цей код:

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

Я ніколи раніше не бачив цього yieldключового слова. Намагаюся запустити отриманий код

Помилка розбору: помилка синтаксису, несподівана T_VARIABLE у рядку x

То що це за yieldключове слово? Це навіть дійсний PHP? А якщо це так, як я ним користуюся?

Відповіді:


355

Що таке yield?

У 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 for 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

Ця функція використовує вихід:

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 генератор не міг повернути значення: це призведе до помилки компіляції. Порожній оператор return був синтаксисом дійсного в генераторі, і він припинить генератор. Оскільки 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:

Генератори забезпечують простий спосіб реалізації простих ітераторів без накладних витрат або складності реалізації класу, що реалізує інтерфейс Iterator.

Генератор дозволяє писати код, який використовує foreach для ітерації над набором даних, не потребуючи побудови масиву в пам’яті, що може призвести до перевищення ліміту пам’яті або вимагати значного часу для обробки. Натомість ви можете записати функцію генератора, яка є такою ж, як і звичайну функцію, за винятком того, що замість того, щоб повернутися один раз, генератор може отримати стільки разів, скільки потрібно для того, щоб надати значення, які слід повторити.

З цього місця: генератори = генератори, інші функції (просто прості функції) = функції.

Отже, вони корисні, коли:

  • потрібно робити речі прості (або прості речі);

    Генератор дійсно набагато простіше, ніж реалізація інтерфейсу Iterator. З іншого боку, звичайно, що генератори менш функціональні. порівняйте їх .

  • вам потрібно генерувати ВЕЛИКІ об’ємні дані, що зберігають дані;

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

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

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

  • вам потрібно підвищити продуктивність.

    вони можуть працювати швидше, ніж функціонувати в деяких випадках (див. попередню перевагу);


1
Я не розумів, як працюють генератори. цей клас реалізує інтерфейс ітератора. з того, що мені відомо, класи ітераторів дозволяють мені конфігурувати, як я хочу повторювати об’єкт. наприклад, ArrayIterator отримує масив або об'єкт, щоб я міг змінювати значення та ключі під час його ітерації. тож якщо ітератори отримують весь об'єкт / масив, то як генератору не потрібно будувати весь масив у пам'яті ???
користувач3021621

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);

Примітка. З генераторами легко помилитися, тому завжди пишіть одиничні тести, перш ніж їх впроваджувати! Примітка2: Використання генераторів у нескінченному циклі - це як написання закриття, яке має нескінченну довжину ...


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 Мб у першому прикладі, ~ 450B у другому прикладі).


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;
}

Отже, не можна використовувати урожай для генерування html-коду в php? Я не знаю переваг у реальному середовищі
Джузеппе Лоді Різзіні

@GiuseppeLodiRizzini, що змушує тебе це думати?
Бред Кент
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.