10.

PHP yield (ジェネレータ) と return の違い完全ガイド

編集
この記事の要点
  • return は関数を終了して 1 つの値を返す。呼び出し元に制御を戻して終わり
  • yield は関数を一時停止して値を生成。次回呼び出しで続きから再開する ジェネレータ
  • yield を含む関数は自動的に Generator オブジェクトを返す。配列ではなく反復可能オブジェクト
  • 巨大配列を作らずに済むのでメモリ効率が劇的に良い (100 万件でも数 KB)
  • 使い分け: 一度に全部必要なら return、順次処理で済むなら yield。Laravel の lazy() / lazyById() も内部はジェネレータ

return と yield の基本動作の違い

// return: 1 回呼んで終わり
function range_return(int $n): array {
    $result = [];
    for ($i = 0; $i < $n; $i++) {
        $result[] = $i;
    }
    return $result;     // ★ ここで終了、全要素を含む配列を返す
}

// yield: 1 回ごとに値を吐き出す
function range_yield(int $n) {
    for ($i = 0; $i < $n; $i++) {
        yield $i;       // ★ 一時停止して値を返す、次回ここから再開
    }
}

// 利用は同じ foreach で書ける
foreach (range_return(5) as $v) { echo $v; }  // 01234
foreach (range_yield(5)  as $v) { echo $v; }  // 01234

メモリ効率の決定的な差

// 100 万件で比較
function arr_return(): array {
    $a = [];
    for ($i = 0; $i < 1_000_000; $i++) {
        $a[] = "item_$i";
    }
    return $a;
}

function arr_yield() {
    for ($i = 0; $i < 1_000_000; $i++) {
        yield "item_$i";
    }
}

// メモリ計測
echo memory_get_usage() . PHP_EOL;     // ベース: 約 400 KB

$a = arr_return();
echo memory_get_usage() . PHP_EOL;     // 約 60 MB ❌

unset($a);

foreach (arr_yield() as $v) {
    // 1 件ずつ処理
}
echo memory_get_usage() . PHP_EOL;     // 約 400 KB ✅

yield の構文バリエーション

function basics() {
    yield 1;                          // 値のみ (キーは 0 から自動)
    yield 2;
    yield 3;
}

function withKey() {
    yield 'name' => '太郎';            // ★ キー => 値
    yield 'age'  => 30;
}

function fromAnother() {
    yield 1;
    yield from [2, 3, 4];             // ★ 他の iterable を流し込む
    yield from basics();              // ジェネレータ from ジェネレータ
    yield 5;
}

// 受け取り方
foreach (withKey() as $k => $v) {
    echo "$k = $v\n";
}
// name = 太郎
// age  = 30

Generator クラスのメソッド

yield を含む関数は Generator オブジェクトを返します。foreach 以外にも直接メソッドを呼べます:

$gen = range_yield(5);

$gen->current();   // 現在の値
$gen->key();       // 現在のキー
$gen->next();      // 次へ進める
$gen->valid();     // まだ要素があるか
$gen->rewind();    // 巻き戻し (※ 一度進めた後は不可)
$gen->send($v);    // ジェネレータに値を送る (双方向通信)
$gen->getReturn(); // ジェネレータ終了後の return 値を取得

// 手動でループ
while ($gen->valid()) {
    echo $gen->current() . "\n";
    $gen->next();
}

ジェネレータ内の return

ジェネレータ関数内でも return が使えます。値はジェネレータ終了後に getReturn() で取得可能:

function counter() {
    $i = 0;
    while ($i < 3) {
        yield $i++;
    }
    return 'finished';  // ★ 終了時の戻り値
}

$gen = counter();
foreach ($gen as $v) {
    echo "$v\n";       // 0, 1, 2
}
echo $gen->getReturn(); // finished

典型ユースケース1: 巨大ファイル処理

// ❌ 全部メモリに読む → OOM 確定
$lines = file('huge.csv');
foreach ($lines as $line) { ... }

// ✅ 行単位でストリーミング
function readLines(string $path) {
    $fp = fopen($path, 'r');
    while (($line = fgets($fp)) !== false) {
        yield trim($line);
    }
    fclose($fp);
}

foreach (readLines('huge.csv') as $line) {
    // 1 行ずつ処理 → メモリ一定
}

典型ユースケース2: 無限ストリーム

// 無限フィボナッチ
function fibonacci() {
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

$count = 0;
foreach (fibonacci() as $n) {
    echo "$n ";
    if (++$count >= 10) break;
}
// 0 1 1 2 3 5 8 13 21 34

Laravel での lazy / lazyCollection

Laravel 6+ では Eloquent / Collection にジェネレータベースの API が用意されています:

// ❌ get() は全件メモリ展開 → 大量データで OOM
foreach (User::all() as $user) { ... }

// ✅ lazy() は内部 yield で 1000 件ずつ取得
foreach (User::lazy() as $user) {
    // メモリ一定で全件処理
}

// ID 順で安全に lazy 取得 (推奨)
foreach (User::lazyById() as $user) { ... }

// Collection の lazy
LazyCollection::make(function () {
    $fp = fopen('big.csv', 'r');
    while (($line = fgets($fp)) !== false) {
        yield $line;
    }
})->each(fn($l) => process($l));

return vs yield 使い分け

状況推奨
結果が小さい (数件〜数百件)return + 配列
結果が大きい / メモリ気になるyield
呼び出し側で count() や添字アクセスが必要return + 配列
呼び出し側は foreach で十分yield
無限列 / 終わりのないストリームyield 一択
JSON にエンコードして返す APIreturn (Generator は json_encode 不可)

テスト方法

use PHPUnit\Framework\TestCase;

class GeneratorTest extends TestCase {
    public function test_range_yield(): void {
        $result = iterator_to_array(range_yield(3));
        $this->assertSame([0, 1, 2], $result);
    }

    public function test_yields_3_items(): void {
        $gen = range_yield(3);
        $this->assertCount(3, iterator_to_array($gen));
    }
}

FAQ

Q: ジェネレータは何度も foreach できる?
A: できません。1 度走り切ると終了。再度回すには関数を呼び直す必要があります ($gen = range_yield(5) を再実行)。

Q: ジェネレータを array にしたい
A: iterator_to_array($gen)。ただし全件メモリ展開されるので yield の利点が消えます。

Q: PHP のどのバージョンから使える?
A: PHP 5.5+ で yield、PHP 7.0+ で yield from、PHP 7.0+ でジェネレータの return 値サポート。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. インストール方法
  2. 文法
  3. Composerのインストール
  4. 内部関数
  5. フレームワーク
  6. エラー一覧
  7. 改行出力
  8. printとechoの違い
  9. シングルクォートとダブルクォートの違い
  10. returnとyieldの違い
  11. var_dumpをログ出力
  12. CSV読み込み
  13. 待機・処理の遅延
  14. ログファイルにエラーを出力する方法
  15. エラーログ出力関数
  16. URLパラメータの配列化
  17. empty, is_null. issetの判定比較表
  18. httpステータスコードの付与
  19. バージョンの確認
  20. php.ini
  21. APIを呼び出す方法
  22. 外部ファイルを呼び出す方法
  23. カンマ区切りの文字列を配列に変換
  24. 配列からランダムに値を取り出す方法
  25. Webスクレイピング

最近更新/作成されたページ