PHPのジェネレータ関数について、使い方、通常の関数との違い、注意点を参考プログラムと出力結果とともに解説しています。大量のデータを扱う処理などにジェネレータ関数を活用すれば、メモリの消費量を節約でき、プログラムの可読性も上がります。
PHPのジェネレータ関数とは
PHPで関数を定義する際にfunctionの中でyield構文を使って値を生成している関数をジェネレータ関数と呼びます。またジェネレータ関数を使って生成されるGeneratorオブジェクトは繰り返し処理が可能なデータ構造のオブジェクトです。
通常のPHPの関数ではreturnを使うと値を返しますが、yieldを使ったジェネレータ関数では値を返すのではなく、Generatorオブジェクトを返します。yieldによって生成された値はそのオブジェクトの中に入っています。
次のプログラムでPHPの通常の関数normalFuncではreturnを使っているので文字列sampleを返しますが、ジェネレータ関数generatorFuncではGeneratorオブジェクトを返します。
function normalFunc() {
return 'sample';
}
function generatorFunc() {
yield 'sample';
}
var_dump(normalFunc());
var_dump(generatorFunc());
出力結果
string(6) "sample"
object(Generator)#1 (0) {
}
Generatorオブジェクトは配列のようなデータ構造なのでその中にはyieldによって生成された値が入っており、その値を取り出すためにループ処理を行います。
次のプログラムではyieldを使って、文字列appleを生成し、foreachを使って、Generatorオブジェクトから値を取り出します。
function generatorFunc() {
yield 'apple';
}
$yields = generatorFunc();
foreach ($yields as $yield) {
echo $yield;
}
出力結果
apple
ジェネレータのメリット
メモリの節約ができる?
繰り返し処理で配列を使って大量のデータを扱う場合、ジェネレータ関数を使うと配列の生成をしなくなる分メモリの消費量を抑えることができます。
PHPの通常の関数を使った場合とジェネレータ関数を使った場合のメモリ消費量を比較します。
ジェネレータを使わない場合の処理
PHPの配列に1から10000までの数値を代入する関数を用意して、その関数から出力された配列の値を取り出すプログラムを使います。
まずはジェネレータ関数を使わずに配列を生成して、値を代入する場合の例です。
ジェネレータなしのサンプルコード
function test() {
$arr = [];
for($i=0;$i<10000;$i++) {
$arr[] = $i;
}
return $arr;
}
echo "before:".memory_get_usage() / (1024 * 1024)."MB\n";
$result = test();
foreach ($result as $value) {}
echo "after:".memory_get_usage() / (1024 * 1024)."MB\n";
出力結果
before:0.36590576171875MB
after:0.86989593505859MB
test関数実行前と実行後を比べると、約0.5MBメモリの消費量が上がっています。
ジェネレータを使った場合の処理
次に上記のプログラムをPHPのジェネレータ関数に置き換えて、メモリの消費量を調べます。
returnの代わりにyieldを使用し、配列の生成を行いません。
ジェネレータを使ったサンプルコード
function test() {
for($i=0;$i<10000;$i++) {
yield $i;
}
}
echo "before:".memory_get_usage() / (1024 * 1024)."MB\n";
$result = test();
foreach ($result as $value) {}
echo "after:".memory_get_usage() / (1024 * 1024)."MB\n";
出力結果
before:0.36524963378906MB
after:0.36564636230469MB
PHPの通常の関数と比べると、ジェネレータ関数ではメモリの消費量を大幅に節約できています。またプログラムの見通しもよくなっています。
10000回配列に数値を代入するという処理でこれだけメモリ消費量に差が出ます。
yieldの使い方【基本編】
yieldの使い方は、通常の関数を定義する時に使われるreturnと似ていますが、挙動が違いますので注意してください。
returnを使った場合はその時点で関数の実行を終了して値を返します。
yieldを使った場合は関数の実行を一旦そこで止めて、関数の呼び出し元に値を返した後に関数の実行を再開します。yieldによって生成される値がなくなるまで関数は実行されます。
まずはreturnを使った通常の関数の例です。関数returnFuncを何度呼び出しても最初にreturnが記述してある箇所での変数ansの値を返します。
function returnFunc()
{
$num = 10;
$ans = $num * 2;
return $ans;
$ans = $num * 3;
return $ans;
$ans = $num * 4;
return $ans;
}
$val1 = returnFunc();
print("1回目: ".$val1 . "\n");
$val2 = returnFunc();
print("2回目: ".$val2 . "\n");
$val3 = returnFunc();
print("3回目: ".$val3 . "\n");
出力結果
1回目: 20
2回目: 20
3回目: 20
次にyieldを使ったジェネレータ関数の例です。
ループ1回目は1つ目のyieldの記述がある箇所の値を返し、2回目は2つ目のyieldの記述がある箇所、3回目は3つ目のyieldの記述がある箇所での変数ansの値を返しています。またyieldの記述がなくなるまで実行されています。
function generatorFunc()
{
$num = 10;
$ans = $num * 2;
yield $ans;
$ans = $num * 3;
yield $ans;
$ans = $num * 4;
yield $ans;
}
$vals = generatorFunc();
$i = 1;
foreach($vals as $val) {
print($i . "回目:" . $val . "\n");
$i++;
}
出力結果
1回目:20
2回目:30
3回目:40
yieldの使い方【応用編】
これまでのyieldを使った参考プログラムでは要素しか扱ってきませんでしたが、キーと要素をペアにして生成することもでき、連想配列のように扱うこともできます。
function generatorFunc()
{
for($i = 1; $i <= 10; $i++) {
yield $i => 10 * $i;
}
}
foreach(generatorFunc() as $index => $value) {
print($index . "個目: " . $value . "\n");
}
出力結果
1個目: 10
2個目: 20
3個目: 30
4個目: 40
5個目: 50
6個目: 60
7個目: 70
8個目: 80
9個目: 90
10個目: 100
また次のようにyieldを使って値を生成しなかった場合は要素が出力されません。iterator_to_array関数とvar_dump関数を使って、Generatorオブジェクトの型と値を出力すると、キーが割り振られており、NULL値が生成されています。
function generatorFunc() {
yield;
yield;
yield;
}
$yields = generatorFunc();
foreach ($yields as $yield) {
echo $yield;
}
var_dump(iterator_to_array(generatorFunc()));
出力結果
array(3) {
[0]=>
NULL
[1]=>
NULL
[2]=>
NULL
}
より実践に近いyieldの使い方として、1から1000まで順番に足していって、その合計値を出力する例です。
function test() {
for($i = 1; $i <= 1000; $i++) {
yield $i;
}
}
$sum = 0;
foreach(test() as $value) {
$sum += $value;
}
echo $sum;
出力結果
500500
まとめ
PHPのジェネレータ関数について解説しました。
繰り返し処理で配列を使って、大量にデータを扱っているプログラムなどにジェネレータ関数を使えばメモリの消費量を節約でき、プログラムの見通しもよくなります。
ジェネレータはPHPのバージョン5.5で追加された機能なので、導入を検討する際はPHPのバージョンにも気をつけてください。