foreachで&を使った後、unsetを忘れると値が壊れる
foreach ($arr as &$v) で配列を参照渡しで回すと、ループ終了後も$vは配列の最後の要素への参照を保持し続けます。unset($v)を忘れると、次に$vという名前でループを回したときに最後の要素が上書きされます。
なぜエラーが出ないのか
出力: 244
(エラーなし)- PHPは何も報告しません。文法として正しいためです
出力: 244- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるか、コードを目で追うしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 <?php 2 $nums = [1, 2, 3]; 3 foreach ($nums as &$v) { 4 $v *= 2; 5 } 6 ^ 7 foreach ($nums as $v) { 8 echo $v; 9 } 10 echo "\n" . implode(",", $nums);
1回目のforeachが終わっても$vは配列の最後の要素への参照を保持したままです。unsetし忘れると、2回目のforeachが同じ名前の$vで回った瞬間、最後の要素が上書きされます。
直し方: (空) を unset($v); にします。
パターン2
1 <?php 2 $scores = [5, 10, 15]; 3 foreach ($scores as &$s) { 4 $s += 1; 5 } 6 ^ 7 foreach ($scores as $s) { 8 echo $s; 9 } 10 echo "\n" . implode(",", $scores);
$sも参照のまま残るため、2回目のforeachで自分自身に代入を続け、最後の要素が壊れます。
直し方: (空) を unset($s); にします。
パターン3
1 <?php 2 $prices = [100, 200, 300]; 3 foreach ($prices as &$p) { 4 $p -= 10; 5 } 6 ^ 7 foreach ($prices as $p) { 8 echo $p . " "; 9 } 10 echo "\n" . implode(",", $prices);
unset($p)を忘れると、参照が残ったままの$pに2回目のforeachの値が書き込まれ続け、最後の要素が壊れます。
直し方: (空) を unset($p); にします。
パターン4
1 <?php 2 $nums = [2, 4, 6]; 3 foreach ($nums as &$n) { 4 $n += 1; 5 } 6 ^ 7 foreach ($nums as $n) { 8 echo $n; 9 } 10 echo "\n" . implode(",", $nums);
1回目のforeachが終わっても$nは配列の最後の要素への参照を保持したままです。unsetし忘れると、2回目のforeachで最後の要素が上書きされます。
直し方: (空) を unset($n); にします。
パターン5
1 <?php 2 $data = [2, 3, 4]; 3 foreach ($data as &$d) { 4 $d *= 3; 5 } 6 ^ 7 foreach ($data as $d) { 8 echo $d . " "; 9 } 10 echo "\n" . implode(",", $data);
$dも参照のまま残るため、2回目のforeachで自分自身に代入を続け、最後の要素が壊れます。
直し方: (空) を unset($d); にします。
よくある誤解
「foreachのループ変数はループが終われば無関係になるはず」という思い込みは、&を使った参照foreachには当てはまりません。unsetし忘れると、後続のコードが気づかないうちに配列を壊します。
実務での勘所
unsetし忘れた$vは、単に「最後の値を覚えている」だけではなく、配列の最後の要素の実体そのものへの参照であり続けます。つまりその後のコードでたまたま$vという名前の変数に何か別の値を代入すると、それは新しい変数への代入ではなく、元の配列の最後の要素への書き込みとして実行されてしまいます。これに気づかず全く無関係な処理で$vを再利用すると、最初のforeachで使った配列が意図せず書き換わるという、離れた場所同士が繋がってしまう厄介なバグになります。