PHPのyieldとはどういう意味ですか?


232

私は最近このコードにつまずいた:

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

このyieldキーワードを見たことがありません。取得したコードを実行しようとしています

解析エラー:構文エラー、行xでの予期しないT_VARIABLE

それで、この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です。

通常の機能との違いは何ですか?

ここで、なぜPHPのネイティブrange関数を使用してその出力を実現していないのか不思議に思うかもしれません。そして、そうです。出力は同じになります。違いはそこにたどり着く方法です。

私たちが使用する場合はrangePHPを、それを実行し、メモリ内の数字の配列全体を作成しますreturnその配列全体foreachそれと出力値の上に移動しますループ。つまり、foreachはアレイ自体に作用します。range機能とforeach一度だけ「話」。郵便でパッケージを受け取るようなものだと考えてください。配達担当者がパッケージを渡して出発します。次に、パッケージ全体を展開し、そこにあるものをすべて取り出します。

ジェネレーター関数を使用すると、PHPは関数にステップインし、最後またはyieldキーワードに到達するまで関数を実行します。と出会うと、yieldそのときの値が何であれ、外側のループに返されます。次に、ジェネレータ関数に戻り、生成された場所から続行します。あなたxrangeforループを保持しているので、ループは実行され、$max到達するまで生成されます。それとforeachピンポンをするジェネレーターのように考えてください。

なぜそれが必要なのですか?

明らかに、ジェネレーターを使用してメモリ制限を回避できます。環境によっては、range(1, 1000000)スクリプトを実行すると致命的なエラーが発生しますが、ジェネレーターでは同じように機能します。またはウィキペディアがそれを置くように:

ジェネレータは生成された値をオンデマンドでのみ計算するので、高価であるか、一度に計算することが不可能であるシーケンスを表すのに役立ちます。これらには、たとえば無限シーケンスやライブデータストリームが含まれます。

ジェネレータもかなり高速であるはずです。しかし、私たちが速く話をしているとき、私たちは通常非常に少ない数で話をしていることを覚えておいてください。したがって、実行してすべてのコードを変更してジェネレーターを使用する前に、ベンチマークを行って、それが意味のある場所を確認してください。

ジェネレータのもう1つの使用例は、非同期コルーチンです。yieldキーワードは、値を返しませんが、それはまた、それらを受け入れます。詳細については、下記の2つの優れたブログ投稿を参照してください。

いつ使用できますyieldか?

ジェネレーターはPHP 5.5で導入されました。yieldそのバージョンの前に使用しようとすると、キーワードに続くコードに応じて、さまざまな解析エラーが発生します。したがって、そのコードから解析エラーが発生した場合は、PHPを更新してください。

出典および参考文献:


1
yeildたとえば、次のようなソリューションの利点が何であるかについて詳しく説明してください。ideone.com
Mike

1
ああ、まあ、そして私が生成していた通知。ええと。まあ、私はPHP用ジェネレーター> = 5.0.0をヘルパークラスでエミュレートする実験を行いましたが、ええと、やや読みにくいですが、将来的にはこれを使用する可能性があります。興味深いトピック。ありがとう!
マイク

読みやすさではなく、メモリ使用量!反復処理に使用されるメモリを比較return range(1,100000000)して for ($i=0; $i<100000000; $i++) yield $i
emix

@マイクはい、それは私の答えでもすでに説明されています。他のMikeの例では、10の値しか反復していないため、メモリはほとんど問題になりません。
ゴードン

1
@Mike xrangeの1つの問題は、静的制限の使用が、たとえばネスト(たとえば、n次元多様体の検索や、ジェネレーターを使用した再帰的クイックソートなど)に役立つことです。カウンターのインスタンスが1つしかないため、xrangeループをネストすることはできません。Yieldバージョンではこの問題は発生しません。
シェイン、2015

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では、ジェネレータは値を返すことができませんでした。そうすると、コンパイルエラーが発生します。空のreturnステートメントはジェネレーター内で有効な構文であり、ジェネレーターを終了します。PHP 7.0以降、ジェネレーターは値を返すことができ、値はGenerator :: getReturn()を使用して取得できます。 php.net/manual/en/language.generators.syntax.php
プログラマーDancuk

シンプルで簡潔。
John Miller

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#

それで、関数を中断することなく戻りますか?
Lucas Bustamante

22

yieldキーワードは、PHP 5.5の「ジェネレータ」の定義に役立ちます。では、ジェネレーターとは何ですか?

php.netから:

ジェネレーターは、Iteratorインターフェースを実装するクラスを実装するオーバーヘッドや複雑さなしに、単純なイテレーターを実装する簡単な方法を提供します。

ジェネレーターを使用すると、メモリ内に配列を構築する必要なく、foreachを使用して一連のデータを反復するコードを記述できます。これにより、メモリ制限を超えたり、生成にかなりの処理時間が必要になる場合があります。代わりに、通常の関数と同じジェネレーター関数を書くことができます。ただし、1回返すのではなく、ジェネレーターは、反復する値を提供するために必要な回数だけ生成することができます。

この場所から:ジェネレータ=ジェネレータ、他の関数(単なる関数)=関数。

したがって、次の場合に役立ちます。

  • 単純なこと(または単純なこと)を行う必要があります。

    ジェネレータは、Iteratorインターフェースを実装するよりもはるかに単純です。一方、当然ながら、ジェネレーターの機能は低下します。それらを比較します

  • 大量のデータを生成する必要があります-メモリを節約します。

    実際にメモリを節約するために、ループの反復ごとに関数を介して必要なデータを生成し、反復後にガベージを利用することができます。したがって、ここでの主なポイントは、明確なコードとおそらくパフォーマンスです。あなたのニーズに最適なものをご覧ください。

  • 中間値に依存するシーケンスを生成する必要があります。

    これは以前の考えを拡張したものです。ジェネレーターは、関数と比較して物事を簡単にすることができます。フィボナッチの例を確認し、ジェネレータなしでシーケンスを作成してみてください。また、この場合、ジェネレーターはより高速に動作できます。これは、少なくともローカル変数に中間値を格納するためです。

  • パフォーマンスを改善する必要があります。

    場合によっては、機能してから機能する方が速くなります(前の利点を参照)。


1
発電機の仕組みがわかりませんでした。このクラスは反復子インターフェースを実装します。イテレータクラスを使用すると、オブジェクトを反復処理する方法を構成できます。たとえば、ArrayIteratorは配列またはオブジェクトを取得するため、反復しながら値とキーを変更できます。イテレータがオブジェクト/配列全体を取得した場合、ジェネレータはどのようにしてメモリ内に配列全体を構築する必要がなくなりますか???
user3021621 2014年

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ファイル(私の使用例では262MB)で生成された配列を使用した例です。

<?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 MB、2番目の例では〜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されて参照することによって得られたため、発電機の名前の前にアンパサンドに。そのため$valueforeachループ$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;
}

だから、phpでhtmlコードを生成するためにyieldを使用することは不可能ですか?実際の環境でのメリットがわかりません
ジュゼッペロディリッツィーニ

@GiuseppeLodiRizziniどうしてそう思うの?
Brad Kent
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.