タイトル: returnとyieldの違い
SEOタイトル: PHP yield (ジェネレータ) と return の違い完全ガイド
| この記事の要点 |
|
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 にエンコードして返す API | return (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 値サポート。