foreachの$valueを参照で受けると思わぬバグを引き起こす

PHP :: Bug #29992 :: foreach by reference corrupts the array

<?php
$array = array(1, 2, 3);
foreach ($array as &$value) {}
var_dump($array);
foreach ($array as $value) {}
var_dump($array);
?>

PHP5からはforeachの$valueの部分を参照で受け取ることができるようになったんだが、このコードがいとも簡単に配列$arrayをぶち壊してしまうというお話。
上記コードの結果を予測できるかな?

array(3) {
  [0]=> int(1)
  [1]=> int(2)
  [2]=> &int(3)
}
array(3) {
  [0]=> int(1)
  [1]=> int(2)
  [2]=> &int(2)  // ←ここ注目!
}

※var_dumpは縦に長くなって見にくいので改行を削った。

結果はこの通り。
何もしてないのに2回目のループを通過すると最後の要素の値が変わってしまっている。
しかもなんか最後の要素だけ&とかついてる。

何が起こっているのか

何故このようなことが起こるのかはBug#29992の投稿に書かれている。
まず最初のforeach ($array as &$value)だが、ループによる$valueの変化は次のようになる。

  • 1周目:$array[0]への参照を$valueに設定($value=1)。
  • 2周目:$array[1]への参照を$valueに設定($value=2)。
  • 3周目:$array[2]への参照を$valueに設定($value=3)。

要素が3つなのでここでループが終了。
ところでPHPブロックスコープがないので、ループを抜けた後も$valueが生きていることになる。このとき$valueは$array[2]を指している。
さて、このまま2つめのforeach ($array as $value)に入るとどうなるだろうか。2回目のループは$valueに&をつけてないところがポイント。

  • 1周目:$array[0]の値を$valueの参照する先へ代入。つまり$array[2]に$array[0]を代入($array[0]=1, $array[2]=1)。
  • 2周目:$array[1]の値を$valueの参照する先へ代入。つまり$array[2]に$array[1]を代入($array[1]=2, $array[2]=2)。
  • 3周目:$array[2]の値を$valueの参照する先へ代入。つまり$array[2]に$array[2]を代入($array[2]=2, $array[2]=2)。

とまあこのようにして3番目の要素が破壊されたというわけ。
2回目のループも&$valueだったり、あるいは$vのように異なる変数であればこのような現象は起こらないわけだが、このメカニズムを考えるとそれでよいというわけでもない。
ではどうすればよいのか。

どうすればいいのか

実はこの答えはマニュアルに書いてある。

<?php
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
// $arr は array(2, 4, 6, 8) となります
unset($value); // 最後の要素への参照を解除します
?>


警告

foreach ループを終えた後でも、 $value は配列の最後の要素を参照したままとなります。 unset() でその参照を解除しておくようにしましょう。

ようするに使い捨ての局所変数はきっちり後始末しろと。
うは、めんどくせー。ブロックスコープがあれば解決なんじゃないのか? とか思ったり。


ところで、実はこの現象と同じことが(若干形は違うけど)マニュアルの別のページにも書いてあったりする。



注意: foreach ステートメント の内部でリファレンス変数に値を代入すると、リファレンスも変更されます。

例3 リファレンスと foreach ステートメント

<?php
$ref = 0;
$row =& $ref;
foreach (array(1, 2, 3) as $row) {
    // 何かを実行します
}
echo $ref; // 3 - 配列の最後の要素
?>

参照じゃなくていいじゃん

PHPは基本的にcopy on writeなので神経質になって参照にする必要はないのかもしれない。
むしろ参照を生成するコストのほうが高いとマニュアルのどっかに書いてあった。

ちょっと自信がないからあとで調べてみよう。

おまけ

<?php
$array = array(1, 2, 3);
for ($i = 0; $i < count($array); $i++) {
	$value =& $array[$i];
}
var_dump($array);
for ($i = 0; $i < count($array); $i++) {
	$value = $array[$i];
}
var_dump($array);
?>