対象は PHP 8.1+(可能なら 8.2/8.3)です。
PSR‑12/PSR‑3/PSR‑7/PSR‑15 の思想(責務分離・出力境界の明確化)を前提にしています。
1. for / foreach の基本的な使い方
for
文は、初期化、継続条件、更新の3つの式を明示することで、数値や添字による反復を正確に表現します。C言語由来の構文のため、読み手の予測可能性が高く、添字の厳密な制御(スキップ、逆順、ステップ幅の変更)が容易です。配列長をループ条件に使う際には、count()
の呼び出しオーバーヘッドを避けるため、事前に変数にキャッシュするのが良い習慣です。
// NG: count() を毎回呼ぶ
for ($i = 0; $i < count($arr); $i++) {
// ...
}
// OK: count() を事前にキャッシュ
for ($i = 0, $len = count($arr); $i < $len; $i += 2) {
// 偶数インデックスのみ処理
process($arr[$i]);
}
foreach
文は、PHPの「配列=ハッシュテーブル+順序」という特性、そして Traversable
/Iterator
を介したコレクション反復に最適化された構文です。foreach ($xs as $x)
は「コレクションから要素を順に取り出す」という意図を直接的に表現するため、添字が不要な場合はこれを第一選択とし、キーも必要な場合は as $k => $v
を使い、配列・ArrayObject
・任意の IteratorAggregate
に透過的に対応できます。
foreach ($users as $id => $user) {
// $id はキー、$user は値
$user->notify();
}
重要なポイントは、for
は “数・添字” を回すのに向き、foreach
は “要素” を回すのに向くという役割分担です。可読性、誤用リスク、意図の明確さから、まず foreach
を選ぶのが現場の健全な初期判断になります。
2. 応用的な使い方:参照・巨大配列・イテレータ・ネスト回避
2.1 参照渡しによる破壊的更新(& の正しい使いどころ)
foreach ($arr as &$v)
は、元配列をインプレースで書き換える意図を明確にする場合にのみ使います。
PHP 7+ の Copy-On-Write 最適化により、速度面でのメリットは限定的です。参照の持ち越しによるバグ(ループ後も $v
が参照のまま残る)を防ぐため、ループ後には必ず unset($v);
を実行して参照を解除する必要があります。
foreach ($prices as &$p) {
$p = round($p * 1.1, 2);
}
unset($p); // 重要:参照解除(次の foreach で事故るのを防ぐ)
2.2 ジェネレータ(yield
)で“配列にしない”という選択
巨大なデータを扱う場合、「全部メモリに載せない」ことが最大の最適化になるケースが多いです。yield
を使うジェネレータは、イテレータを安価に定義できるため、1億行のCSVの逐次処理やデータベース結果のストリーミングに威力を発揮します。
function readLines(string $path): Generator {
$fp = fopen($path, 'rb');
try {
while (!feof($fp)) {
yield fgets($fp);
}
} finally {
fclose($fp);
}
}
foreach (readLines('/var/log/app.log') as $line) {
// 行ごとに処理、ピークメモリは一定に保たれる
}
2.3 ネスト回避:高階関数とクエリオフロード
二重三重ループは最適化余地のサインです。
- 集約は可能なら DB(SQL)や検索基盤へオフロード
- どうしてもアプリ側なら、インデックス化(
array_column
+array_combine
) とハッシュ参照でネストを解消 array_map
/array_filter
/array_reduce
で“何をしているか”を宣言的に表現(副作用を減らす)
$byId = array_column($users, null, 'id'); // id => user の辞書
$total = array_reduce($orders, fn($acc, $o) => $acc + $byId[$o->userId]->weight, 0);
2.4 SPL / Iterator 経由の拡張性
ArrayIterator
, RecursiveIteratorIterator
, CallbackFilterIterator
などを組むと、条件付きの深さ優先走査や遅延フィルタリングをforeach のまま表現できます。
業務ドメインに合わせた IteratorAggregate を作り、“回し方”をオブジェクトに隠蔽するのは堅牢な設計です。
3. 代案:while / do‑while, 高階関数, ディスパッチ表, 並列
3.1 while / do‑while(条件主導・1回は必ず実行)
while
は「条件が真の間」回す。for
より前処理/後処理の自由度が高い反面、無限ループ事故の温床。do‑while
は最低1回実行を保証。ユーザー入力ループなどで使い分けます。
do {
$line = fgets($fp);
// …
} while ($line !== false);
3.2 高階関数群(array_map
/array_filter
/array_reduce
)
array_map
/array_filter
/array_reduce
は、副作用のない変換を宣言的に記述できるため、テストや並列化の観点で優れています。PHP 8.1+ の JIT(Just-In-Time)コンパイラが有効な環境では、シンプルなラムダ呼び出しのオーバーヘッドはほとんどなく、foreach
と同等以上の性能を発揮する場合があります。
$actives = array_values(array_filter($users, fn($u) => $u->active));
$names = array_map(fn($u) => $u->name, $actives);
3.3 ディスパッチ表(配列によるルックアップ)
条件分岐をO(1) の辞書参照に落とす。多分岐の if/else
/switch
を除去し、foreach の中身を最小化します。
$handlers = [
'A' => fn($x) => processA($x),
'B' => fn($x) => processB($x),
];
foreach ($items as $it) {
($handlers[$it->type] ?? $default)($it);
}
3.4 並列(プロセス/拡張)
標準 PHP は並列が得意ではありませんが、プロセス分割(symfony/process
+ 仕事分割)や Swoole/Amp/ReactPHP でI/O の同時進行を実現できます。
巨大バッチはOS レベルの並列(プロセスプール)+ジェネレータの分割供給が実務解です。
4. 実測ベンチマーク(PHP 8.3)と解釈(現場の意思決定に直結)
■ 計測スクリプト
bench_loops.php
<?php
declare(strict_types=1);
// 設定
const N = 1_000_000;
$arr = range(1, N);
// 高分解能タイマ
function t(callable $fn, int $repeat = 3): float {
$best = INF;
for ($i=0; $i<$repeat; $i++) {
$t0 = hrtime(true);
$fn();
$dt = (hrtime(true) - $t0) / 1e9;
if ($dt < $best) $best = $dt;
}
return $best;
}
// for(count を毎回呼ぶ)
$for_count_each = t(function() use ($arr) {
$sum = 0;
for ($i = 0; $i < count($arr); $i++) {
$sum += $arr[$i];
}
});
// for(count 結果をキャッシュ)
$for_cached_len = t(function() use ($arr) {
$sum = 0;
for ($i = 0, $len = count($arr); $i < $len; $i++) {
$sum += $arr[$i];
}
});
// foreach(値渡し)
$foreach_value = t(function() use ($arr) {
$sum = 0;
foreach ($arr as $v) {
$sum += $v;
}
});
// foreach(参照渡し/書き換えなし)
$foreach_ref_no_write = t(function() use ($arr) {
$sum = 0;
foreach ($arr as &$v) {
$sum += $v;
}
unset($v);
});
// foreach(参照渡しで書き換えあり)
$foreach_ref_write = t(function() use ($arr) {
foreach ($arr as &$v) {
$v += 1;
}
unset($v);
});
// array_map(合計を返す形に包む)
$array_map = t(function() use ($arr) {
$out = array_map(static fn($v) => $v + 1, $arr);
});
// ジェネレータ(逐次供給→foreach)
function gen(array $arr): Generator {
foreach ($arr as $v) yield $v;
}
$generator_foreach = t(function() use ($arr) {
$sum = 0;
foreach (gen($arr) as $v) {
$sum += $v;
}
});
// while(インデックス)
$while_index = t(function() use ($arr) {
$sum = 0; $i = 0; $len = count($arr);
while ($i < $len) {
$sum += $arr[$i++];
}
});
// 結果表示
$rows = [
['for (count毎回)', $for_count_each],
['for (countキャッシュ)', $for_cached_len],
['foreach (値渡し)', $foreach_value],
['foreach (&参照, 書換なし)', $foreach_ref_no_write],
['foreach (&参照, 書換あり)', $foreach_ref_write],
['array_map (+1)', $array_map],
['generator→foreach', $generator_foreach],
['while (index)', $while_index],
];
usort($rows, fn($a,$b) => $a[1] <=> $b[1]);
printf("%-26s | %9s\n", 'Case', 'sec');
echo str_repeat('-', 40), "\n";
foreach ($rows as [$name, $sec]) {
printf("%-26s | %9.6f\n", $name, $sec);
}
実行コマンド例
php -d opcache.enable_cli=1 bench_loops.php
以下のベンチマーク結果は、特定の環境下の一例に過ぎず、PHPのバージョンやJITの有無によって変動します。
中央値の一例(環境差あり)
Case | sec(小さいほど速い) |
---|---|
for (countキャッシュ) | 0.028 |
foreach (値渡し) | 0.030 |
while (index) | 0.031 |
generator→foreach | 0.036 |
foreach (&参照, 書換なし) | 0.039 |
array_map (+1) | 0.069 |
for (count毎回) | 0.108 |
foreach (&参照, 書換あり) | 0.126 |
解釈(重要な順)
for (count毎回)
は明確に遅い → 必ず$len = count($arr)
をキャッシュ。for(lenキャッシュ)
とforeach(値渡し)
は実質同等 → 可読性/意図で選ぶ。添字が要るなら for。- 参照 foreach はオーバーヘッド+安全性低下 → 破壊更新に限定。
unset($v)
を必ず。 - array_map は遅いが“副作用なしの宣言”が武器 → ホットパス外やテスト容易性優先で採用。
- ジェネレータは小規模では不利だが大規模で圧勝 → 「配列化しない」ことでメモリを一定に保てる。
最終指針
- デフォルト:foreach(値渡し)。
- 添字やステップ制御が必要:for(count キャッシュ)。
- 巨大データ:ジェネレータ+foreach。
- 副作用なしを表明:array_map/filter/reduce。
- 破壊更新:foreach 参照(&)だが安全策必須。
- パフォーマンスはアルゴリズム・I/O 削減が支配的。ループ構文の差は二次的。
5. バグ回避と可読性の鉄則(現場のチェックリスト)
count()
はキャッシュしましょう。foreach (&$v)
を使ったら、必ずunset($v)
を実行しましょう。- ループ中に配列自体を
unset
/push
しないようにしましょう。 - ビジネスロジックを関数化し、ループ本体は3〜5行に抑えましょう。
- 巨大なデータは “配列にしない”(ジェネレータ/ストリーム)ことを基本としましょう。
- プロファイラ(Blackfire/Xdebugなど)でボトルネックを実測し、感覚ではなくデータに基づいて判断しましょう。
6. サンプル:実務パターン別コード(良い例/悪い例)
6.1 良い例:辞書化+単一走査
// NG: ネストして O(n*m)
foreach ($orders as $o) {
foreach ($users as $u) {
if ($u->id === $o->userId) { /* ... */ }
}
}
// OK: 事前に辞書を作って O(n)
$byId = array_column($users, null, 'id');
foreach ($orders as $o) {
$u = $byId[$o->userId] ?? null;
if ($u) { /* … */ }
}
6.2 良い例:ジェネレータで CSV を加工しながら吐く
function rows(string $csv): Generator {
$f = new SplFileObject($csv);
$f->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY);
foreach ($f as $row) {
if ($row === [null]) continue;
yield $row;
}
}
foreach (rows('input.csv') as [$id, $name, $age]) {
echo implode(',', [$id, strtoupper($name), $age]), "\n";
}
6.3 悪い例:参照の持ち越しバグ
foreach ($a as &$x) { /* … */ }
foreach ($b as $y) { $y = $x; } // $x が参照のまま残り、意図せず書き換えが波及
7. 結論
- 読みやすさと安全性を最優先 → まず foreach(値渡し)。
- 数値・添字制御が要件 → for(count キャッシュ)。
- メモリ圧とスループット → ジェネレータ+ストリームで配列化回避。
- 副作用なしの変換 → 高階関数で意図を宣言。
- 速度は実測で判断。ループ構文差より前処理・データ構造・I/Oが効く。
コメント