ジェネレータとは

(PHP 5 >= 5.5.0, PHP 7, PHP 8)

ジェネレータを使えば、シンプルな イテレータを簡単に実装できます。 Iterator インターフェイスを実装したクラスを用意する オーバーヘッドや複雑さを心配する必要はありません。

ジェネレータを使うと、foreach でデータ群を順に処理するコードを書くときに メモリ内で配列を組み立てなくても済むようになります。 メモリ内で配列を組み立てると memory_limit を越えてしまうかもしれないし、 無視できないほどの時間がかかってしまうかもしれません。 配列を作る代わりに、ジェネレータ関数を書くことになります。これは通常の 関数と同じものですが、 ジェネレータ関数は一度だけ return するのではなく、必要に応じて何度でも yield することができます。 つまり、値を繰り返し返せるということです。 イテレーターと同じく、ランダムアクセスはできません。

シンプルな例として、range() 関数をジェネレータで実装しなおしてみましょう。 標準の range() 関数は、すべての値を含む配列を作ってそれを返します。 結果的に、かなり大きな配列になる可能性があります。たとえば range(0, 1000000) を実行すると、 100 MB を超えるメモリを使うことになります。

その代替として、ジェネレータ xrange() を実装します。 必要なメモリは、Iterator オブジェクトを作ってジェネレータの内部の状態を記録しておくのに必要なものだけになります。 1 KB 未満で収まるでしょう。

例1 ジェネレータを使った range() の実装

<?php
function xrange($start, $limit, $step = 1) {
if (
$start <= $limit) {
if (
$step <= 0) {
throw new
LogicException('Step must be positive');
}

for (
$i = $start; $i <= $limit; $i += $step) {
yield
$i;
}
} else {
if (
$step >= 0) {
throw new
LogicException('Step must be negative');
}

for (
$i = $start; $i >= $limit; $i += $step) {
yield
$i;
}
}
}

/*
* 次の例で、range() と xrange() が同じ結果を返すことに注目しましょう
*/

echo 'Single digit odd numbers from range(): ';
foreach (
range(1, 9, 2) as $number) {
echo
"$number ";
}
echo
"\n";

echo
'Single digit odd numbers from xrange(): ';
foreach (
xrange(1, 9, 2) as $number) {
echo
"$number ";
}
?>

上の例の出力は以下となります。

Single digit odd numbers from range():  1 3 5 7 9
Single digit odd numbers from xrange(): 1 3 5 7 9

Generator オブジェクト

ジェネレータ関数を呼んだときには、内部クラス Generator の新しいオブジェクトを返します。 このオブジェクトは Iterator インターフェイスを実装しており、 前進しかできないイテレータオブジェクトと同じように振る舞います。 そして、このオブジェクトが提供するメソッドを使えば、 値を送ったり戻したりなどしてジェネレータの状態を操作できます。

add a note

User Contributed Notes 6 notes

up
178
bloodjazman at gmail dot com
11 years ago
for the protection from the leaking of resources see RFC https://wiki.php.net/rfc/generators#closing_a_generatorand use finnalysample codefunction getLines($file) {    $f = fopen($file, 'r');    try {        while ($line = fgets($f)) {            yield $line;        }    } finally {        fclose($f);    }}foreach (getLines("file.txt") as $n => $line) {    if ($n > 5) break;    echo $line;}
up
47
montoriusz at gmail dot com
9 years ago
Bear in mind that execution of a generator function is postponed until iteration over its result (the Generator object) begins. This might confuse one if the result of a generator is assigned to a variable instead of immediate iteration.<?php$some_state = 'initial';function gen() {    global $some_state;     echo "gen() execution start\n";    $some_state = "changed";    yield 1;    yield 2;}function peek_state() {    global $some_state;    echo "\$some_state = $some_state\n";}echo "calling gen()...\n";$result = gen();echo "gen() was called\n";peek_state();echo "iterating...\n";foreach ($result as $val) {    echo "iteration: $val\n";    peek_state();}?>If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function.<?php/**  * @return Generator  */function some_generator() {    global $some_state;    $some_state = "changed";    return gen();}?>
up
18
chung1905 at gmail dot com
5 years ago
In addition to the note of "montoriusz at gmail dot com": https://www.php.net/manual/en/language.generators.overview.php#119275"If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function."You can use Generator::rewind instead (https://www.php.net/manual/en/generator.rewind.php)Sample code:<?php/** function/generator definition **/echo "calling gen()...\n";$result = gen();$result->rewind();echo "gen() was called\n";/** iteration **/?>
up
28
info at boukeversteegh dot nl
9 years ago
Here's how to detect loop breaks, and how to handle or cleanup after an interruption.<?php    function generator()    {        $complete = false;        try {            while (($result = some_function())) {                yield $result;            }            $complete = true;        } finally {            if (!$complete) {                // cleanup when loop breaks             } else {                // cleanup when loop completes            }        }        // Do something only after loop completes    }?>
up
19
lubaev
11 years ago
Abstract test.<?php$start_time=microtime(true);$array = array();$result = '';for($count=1000000; $count--;){  $array[]=$count/2;}foreach($array as $val){  $val += 145.56;  $result .= $val;}$end_time=microtime(true);echo "time: ", bcsub($end_time, $start_time, 4), "\n";echo "memory (byte): ", memory_get_peak_usage(true), "\n";?><?php$start_time=microtime(true);$result = '';function it(){  for($count=1000000; $count--;)  {    yield $count/2;  }}foreach(it() as $val){  $val += 145.56;  $result .= $val;}$end_time=microtime(true);echo "time: ", bcsub($end_time, $start_time, 4), "\n";echo "memory (byte): ", memory_get_peak_usage(true), "\n";?>Result:----------------------------------           |  time  | memory, mb |----------------------------------| not gen  | 2.1216 | 89.25      ||---------------------------------| with gen | 6.1963 | 8.75       ||---------------------------------| diff     | < 192% | > 90%      |----------------------------------
up
14
dc at libertyskull dot com
11 years ago
Same example, different results:----------------------------------           |  time  | memory, mb |----------------------------------| not gen  | 0.7589 | 146.75     ||---------------------------------| with gen | 0.7469 | 8.75       ||---------------------------------Time in results varying from 6.5 to 7.8 on both examples.So no real drawbacks concerning processing speed.
To Top