PHPのyieldの仕組みや使い方について

採用はこちら

目次

PHPのyieldとは

PHPのyieldは、ジェネレータ(Generator)を作るために使用する構文です。

通常の関数ではreturnを実行すると、その時点で関数の処理が終了します。

一方、yieldを使用した関数では、値を1つ提供したところで処理を一時停止し、次の値が必要になったときに停止した位置から処理を再開できます。

この仕組みを利用すると、大量のデータを一度に配列としてメモリへ保存するのではなく、必要なデータを1件ずつ生成しながら処理できます。

そのため、PHPのyieldは、大きなCSVファイルやログファイルの読み込み、大量データの逐次処理、連番の生成などで特に役立ちます。

yieldを含む関数はGeneratorを返す

関数の中にyieldが存在すると、その関数はジェネレータ関数として動作します。

例えば、次のコードを見てみましょう。

function getNumbers(): Generator
{
    yield 1;
    yield 2;
    yield 3;
}

$generator = getNumbers();

getNumbers()を呼び出したとき、12といった値が直接戻ってくるわけではありません。

戻り値としてGeneratorオブジェクトが取得されます。

var_dump($generator);

実行すると、Generatorオブジェクトであることを確認できます。

重要なのは、ジェネレータ関数を呼び出しただけでは、通常は関数本体の処理が最後まで実行されない点です。

foreachcurrent()などによってジェネレータの反復が開始されると、最初のyieldまで処理が進みます。

yieldの基本的な仕組み

処理を一時停止して再開できる

yieldの最大の特徴は、関数の状態を保持したまま処理を一時停止できることです。

次の例を見ると分かりやすいでしょう。

function test(): Generator
{
    echo "開始" . PHP_EOL;

    yield 1;

    echo "1の次" . PHP_EOL;

    yield 2;

    echo "終了" . PHP_EOL;
}

このジェネレータをforeachで処理します。

foreach (test() as $value) {
    echo "値: {$value}" . PHP_EOL;
}

実行結果は次のようになります。

開始
値: 1
1の次
値: 2
終了

最初に、

yield 1;

まで処理すると、値1がジェネレータから提供され、その位置で実行が一時停止します。

次の値が要求されると、その直後から処理を再開します。

つまり、yieldは単に複数の値を返すための構文ではなく、関数の実行状態を保持しながら停止と再開を行う仕組みです。

yieldとreturnの違い

returnは関数を終了する

通常のreturnは、値を呼び出し側へ返した時点で関数を終了します。

function sample(): int
{
    return 1;

    echo 'この処理は実行されません';
}

return 1;が実行された時点で関数が終了するため、それ以降の処理は実行されません。

yieldは関数を終了しない

一方、yieldでは関数そのものは終了しません。

function sample(): Generator
{
    yield 1;

    echo "処理を再開しました" . PHP_EOL;

    yield 2;
}

最初のyield 1で処理を停止したあと、次の値が要求されると続きを実行します。

returnyieldの違いを整理すると、次のようになります。

項目returnyield
処理関数を終了する実行を一時停止する
再開できない停止位置から再開できる
主な用途通常の戻り値値を逐次生成する
大量データ配列化するとメモリを消費する逐次処理しやすい
戻り値通常の値Generatorを通じて値を提供する

技術的には、「yieldで値を返す」というよりも、「yieldで値を生成する」「ジェネレータから値を提供する」と考えた方が正確です。

yieldの基本的な使い方

複数の値を順番に生成する

基本的な使用例は次のとおりです。

function getColors(): Generator
{
    yield 'red';
    yield 'blue';
    yield 'green';
}

foreach (getColors() as $color) {
    echo $color . PHP_EOL;
}

実行結果は次のようになります。

red
blue
green

配列を作成しなくても、複数の値を順番に取り出せます。

for文と組み合わせる

yieldはループ処理と組み合わせることもできます。

function getNumbers(int $max): Generator
{
    for ($i = 1; $i <= $max; $i++) {
        yield $i;
    }
}

次のように利用できます。

foreach (getNumbers(5) as $number) {
    echo $number . PHP_EOL;
}

結果は次のとおりです。

1
2
3
4
5

この方法なら、あらかじめ[1, 2, 3, 4, 5]という配列を作る必要がありません。

yieldでキーと値を指定する方法

配列のようにキーを指定できる

yieldでは、値だけでなくキーも指定できます。

構文は次のとおりです。

yield キー => 値;

例えば、次のように使用できます。

function getUsers(): Generator
{
    yield 1 => 'Taro';
    yield 2 => 'Hanako';
    yield 3 => 'Jiro';
}

foreach (getUsers() as $id => $name) {
    echo "{$id}: {$name}" . PHP_EOL;
}

実行結果は次のようになります。

1: Taro
2: Hanako
3: Jiro

そのため、ジェネレータでも連想配列に近い感覚でキーと値を扱えます。

yieldがメモリ節約に役立つ理由

配列は全データを保持する必要がある

例えば、100万件の数値を通常の配列として生成するとします。

function createNumbers(): array
{
    $numbers = [];

    for ($i = 1; $i <= 1000000; $i++) {
        $numbers[] = $i;
    }

    return $numbers;
}

この方法では、100万件すべての値を配列としてメモリ上に保持します。

PHPの配列は各要素に管理情報を持つため、大量のデータになるほどメモリ使用量も大きくなります。

yieldなら1件ずつ処理できる

ジェネレータを利用すると、次のように書けます。

function createNumbers(): Generator
{
    for ($i = 1; $i <= 1000000; $i++) {
        yield $i;
    }
}

利用側では、次のように1件ずつ処理できます。

foreach (createNumbers() as $number) {
    // 1件ずつ処理
}

100万件をまとめた巨大な配列を作成する必要がないため、メモリ使用量を抑えやすくなります。

ただし、yieldを使えば必ずメモリ使用量が減るわけではありません。

例えば、次のように元データをすでに全件配列へ読み込んでいる場合です。

function getData(): Generator
{
    $rows = loadAllRows();

    foreach ($rows as $row) {
        yield $row;
    }
}

loadAllRows()の時点で全データをメモリに保持していれば、yieldを使っても大きなメモリ削減にはつながりません。

yieldのメリットを生かすためには、元データ自体も逐次取得できる構造にすることが重要です。

大きなファイルをyieldで読み込む方法

テキストファイルを1行ずつ処理する

yieldが特に役立つのが、大きなファイルの読み込みです。

function readLines(string $filePath): Generator
{
    $handle = fopen($filePath, 'r');

    if ($handle === false) {
        throw new RuntimeException('ファイルを開けませんでした。');
    }

    try {
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }
    } finally {
        fclose($handle);
    }
}

利用側では次のようにします。

foreach (readLines('sample.txt') as $line) {
    echo $line;
}

この方法では、ファイル全体を一度に読み込む必要がありません。

fgets()で1行ずつ読み込み、その都度yieldするため、大容量ファイルでも比較的少ないメモリで処理できます。

また、tryfinallyを使用することで、処理終了時にファイルハンドルを閉じやすくなります。

CSVファイルをyieldで処理する方法

CSVを1行ずつ取得する

CSVファイルも同じ考え方で処理できます。

function readCsv(string $filePath): Generator
{
    $handle = fopen($filePath, 'r');

    if ($handle === false) {
        throw new RuntimeException('CSVファイルを開けませんでした。');
    }

    try {
        while (($row = fgetcsv($handle)) !== false) {
            yield $row;
        }
    } finally {
        fclose($handle);
    }
}

利用側では次のようにします。

foreach (readCsv('users.csv') as $row) {
    print_r($row);
}

大きなCSVファイルを配列へ一括変換せず、1行ずつ処理できます。

大量の商品データや会員データをインポートするような処理では有効な方法です。

yield fromの使い方

別のジェネレータへ処理を委譲する

PHPにはyield fromという構文も用意されています。

yield fromを使用すると、別のジェネレータや配列、Traversableオブジェクトが生成する値を、そのまま外側のジェネレータから提供できます。

function first(): Generator
{
    yield 1;
    yield 2;
    yield 3;
}

function second(): Generator
{
    yield from first();

    yield 4;
    yield 5;
}

実行します。

foreach (second() as $number) {
    echo $number . PHP_EOL;
}

結果は次のようになります。

1
2
3
4
5

次のように書く処理を簡潔にできます。

foreach (first() as $value) {
    yield $value;
}

複数のジェネレータを組み合わせたい場合に便利です。

配列にもyield fromを使える

yield fromは配列にも使用できます。

function getNumbers(): Generator
{
    yield from [10, 20, 30];
}

利用側は通常のジェネレータと同じです。

foreach (getNumbers() as $number) {
    echo $number . PHP_EOL;
}

結果は次のようになります。

10
20
30

委譲先Generatorのreturn値も取得できる

yield fromでは、委譲したジェネレータの最終的なreturn値を受け取ることもできます。

function child(): Generator
{
    yield 1;
    yield 2;

    return '完了';
}

function parent(): Generator
{
    $result = yield from child();

    echo $result . PHP_EOL;
}

このように、値の列挙だけでなくジェネレータの最終結果を受け取ることも可能です。

Generatorオブジェクトを直接操作する方法

currentやnextなどを利用できる

通常はforeachでGeneratorを利用するのが簡単ですが、直接メソッドを呼び出すこともできます。

function numbers(): Generator
{
    yield 10;
    yield 20;
    yield 30;
}

$generator = numbers();

現在の値はcurrent()で取得できます。

echo $generator->current();

現在のキーはkey()で取得します。

echo $generator->key();

次の値へ進める場合はnext()を使用します。

$generator->next();

有効な値があるかどうかはvalid()で確認できます。

if ($generator->valid()) {
    echo $generator->current();
}

通常のWebアプリケーションではforeachで十分なケースが多いものの、ジェネレータを細かく制御したい場合にはこれらのメソッドが役立ちます。

Generator::send()で値を送る方法

yield式へ外部から値を渡せる

Generatorには、呼び出し側から値を送る機能もあります。

Generator::send()を利用します。

function generator(): Generator
{
    $value = yield '開始';

    echo "受信: {$value}" . PHP_EOL;
}

次のように利用できます。

$generator = generator();

echo $generator->current() . PHP_EOL;

$generator->send('Hello');

結果は次のようになります。

開始
受信: Hello

send('Hello')で渡した値が、

$value = yield '開始';

$valueへ代入されます。

一般的な大量データ処理では利用頻度は高くありませんが、ジェネレータを双方向に制御したい場合に利用できます。

Generatorのreturn値を取得する方法

getReturnを利用する

ジェネレータ関数の中ではreturnも使用できます。

function numbers(): Generator
{
    yield 1;
    yield 2;

    return '完了';
}

この'完了'は、foreachで列挙される値には含まれません。

ジェネレータの処理が完了したあと、getReturn()で取得できます。

$generator = numbers();

foreach ($generator as $number) {
    echo $number . PHP_EOL;
}

echo $generator->getReturn();

結果は次のようになります。

1
2
完了

つまり、Generatorでは、

yield

は反復する値を提供するために使い、

return

はジェネレータ処理全体の最終的な戻り値を指定するために使用します。

Generatorを配列へ変換する方法

iterator_to_arrayを利用する

Generatorの値を最終的に配列として取得したい場合は、iterator_to_array()を使用できます。

$array = iterator_to_array(getNumbers());

ただし、結果をすべて配列へ格納するため、大量データを扱う場合にはyieldによるメモリ節約のメリットが薄れる可能性があります。

キーの重複に注意する

iterator_to_array()は、デフォルトではGeneratorのキーを保持します。

例えば、同じキーを複数回yieldすると、配列化した際に後の値で上書きされる場合があります。

function values(): Generator
{
    yield 0 => 'A';
    yield 0 => 'B';
}

キーを保持する必要がなく、値だけを順番に配列化したい場合は、第2引数をfalseにします。

$array = iterator_to_array(values(), false);

yield fromを複数回使用する場合などは、キーの重複にも注意するとよいでしょう。

Generatorを再利用するときの注意点

一度進んだGeneratorは自由に巻き戻せない

Generatorは配列のように何度でも自由に先頭へ戻れるものではありません。

基本的には、先頭から末尾へ向かって一方向に処理するための仕組みです。

例えば、Generatorを最後まで処理したあと、同じオブジェクトを再び最初からforeachしようとする使い方には向いていません。

もう一度同じ処理を行いたい場合は、ジェネレータ関数を再度呼び出して新しいGeneratorを作成するのが基本です。

$generator = getNumbers();

foreach ($generator as $number) {
    // 1回目
}

$generator = getNumbers();

foreach ($generator as $number) {
    // 2回目
}

配列との大きな違いの一つとして覚えておくとよいでしょう。

yieldを使うメリット

メモリ使用量を抑えやすい

yieldの代表的なメリットは、すべてのデータを一括してメモリに保持する必要がないことです。

特に大容量ファイルなど、1件ずつ取得できるデータとの相性がよくなっています。

遅延的に値を生成できる

Generatorでは、すべての値を最初に計算する必要がありません。

function generateValues(): Generator
{
    for ($i = 1; $i <= 1000000; $i++) {
        yield calculateValue($i);
    }
}

この場合、100万回分のcalculateValue()がGenerator作成時にすべて実行されるわけではありません。

値が必要になるにつれて順番に処理されます。

コードを比較的シンプルに書ける

独自のIteratorクラスを作ると、現在位置や次の要素などを自分で管理しなければならない場合があります。

Generatorなら、

while (...) {
    yield $value;
}

のような自然なコードで逐次処理を表現できます。

yieldが向いているケース

大容量ファイルを処理するとき

ログファイルやCSVファイルを1行ずつ読み込む場合は、yieldの代表的な用途です。

ファイル全体を一括で配列化する必要がないため、大規模なファイルでも扱いやすくなります。

大量の連番や計算結果を生成するとき

次のように大量の値を順番に生成するときにも適しています。

function generateIds(int $max): Generator
{
    for ($i = 1; $i <= $max; $i++) {
        yield $i;
    }
}

利用側では次のように処理します。

foreach (generateIds(1000000) as $id) {
    // 1件ずつ処理
}

データベースの結果を逐次処理するとき

データベースからデータを逐次取得できる仕組みと組み合わせる場合にも、Generatorは利用できます。

ただし、実際のメモリ使用量はPDOドライバやデータベースドライバ、結果セットのバッファリング方式などにも左右されます。

そのため、Generatorを使っただけで必ずデータベース処理が低メモリになるわけではありません。

データ変換のパイプラインを作るとき

Generator同士を組み合わせれば、データを順番に変換する処理も作れます。

function numbers(): Generator
{
    for ($i = 1; $i <= 5; $i++) {
        yield $i;
    }
}

function doubleValues(iterable $values): Generator
{
    foreach ($values as $value) {
        yield $value * 2;
    }
}

foreach (doubleValues(numbers()) as $value) {
    echo $value . PHP_EOL;
}

このように、データを大量の中間配列へ変換せずに順番に処理できます。

yieldが向いていないケース

データ量が少ない場合

数件程度の固定データを返すだけなら、無理にGeneratorを利用する必要はありません。

例えば、

function getStatuses(): array
{
    return [
        'draft',
        'published',
        'deleted',
    ];
}

程度であれば、通常の配列の方がシンプルです。

ランダムアクセスしたい場合

Generatorは、

$generator[10]

のように任意の位置へ直接アクセスする用途には向いていません。

特定の要素を何度も参照したい場合は、配列などのデータ構造が適しています。

全データを並べ替える場合

例えば、

sort($data);

のように全件を一度に並べ替える処理では、基本的に対象となるデータ全体を保持する必要があります。

このような処理では、Generatorだけではメモリ使用量を大きく減らせない場合があります。

yieldと配列の使い分け

配列が向いているケース

配列は、データ全体を保持して自由にアクセスしたい場合に向いています。

例えば、次のような用途です。

  • データ件数が少ない
  • 同じデータを何度も参照する
  • 任意の位置へアクセスする
  • 全件を並べ替える
  • 全体をまとめて加工する

このような場合は、通常の配列の方が扱いやすいでしょう。

yieldが向いているケース

一方、yieldは次のような用途に適しています。

  • 大量のデータを扱う
  • 1件ずつ処理すればよい
  • CSVやログを順番に読み込む
  • 計算結果を必要な分だけ生成する
  • 中間配列をできるだけ作りたくない
  • データをストリーム的に処理する

特に「全データを一度に持つ必要があるか」という視点で判断すると、配列との使い分けが分かりやすくなります。

PHPのyieldを理解するためのポイント

PHPのyieldを理解するときは、「複数の値を返せる特殊なreturn」と考えるよりも、関数の状態を保持したまま値を順番に生成し、処理を一時停止・再開できる仕組みと考えるのが適切です。

通常の配列を使う処理では、

すべてのデータを取得する
↓
配列へ保存する
↓
配列を処理する

という流れになりやすくなります。

Generatorでは、

1件取得する
↓
yieldする
↓
1件処理する
↓
次の1件を取得する

という流れを作れます。

そのため、PHPで大容量CSV、ログファイル、大量の計算結果などを扱う場合、yieldはメモリ効率や処理設計を改善する有効な選択肢になります。

一方で、Generatorは通常の配列とは異なり、ランダムアクセスや自由な巻き戻しには向いていません。

データ量や処理方法に応じて、通常の配列とyieldを適切に使い分けることが重要です。

以上、PHPのyieldの仕組みや使い方についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次