データの出力処理で、最後の行が出力されず、代わりに最後から2番目の行が2回出力されるというバグが起こりました。
調べてみると、foreachで陥りがちなバグであることがわかったので、対応して得たことをまとめました。
事象
$arrayの要素を、すべて10倍したいとします。
配列の値をforeachの中で変更したいとき、
変数の前に「&」を付けることで、参照渡しで値を設定することができます。(1)
そのあとに、出力処理のために再度foreachしました。(2)
<?php
$array = array(10, 20, 30, 40);
// (1)
foreach ($array as &$value) {
$value *= 10;
}
// (2)
foreach ($array as $value) {
var_dump($value);
}
出力されたものは、
int(100)
int(200)
int(300)
int(300)
と、意図しない結果になっていました。
「!?」と思いましたが、ちゃんと原因がありました。
原因はforeachの参照渡し
(1)のforeachを抜けたところの配列の中身を見てみると、
<?php
$array = array(10, 20, 30, 40);
// (1)
foreach ($array as &$value) {
$value *= 10;
}
var_dump($array);
// (2)
// foreach ($array as $value) {
// var_dump($value);
// }
こうなっています。
array(4) {
[0]=> int(100)
[1]=> int(200)
[2]=> int(300)
[3]=> &int(400)
}
最後の要素にこっそり付いている「&」は、
「配列に含まれる要素の一部が参照(リファレンス)されている」ということを意味します。
つまり、
「$value に代入すると、$array[3]を書き変られる状態が、foreaehを抜けた後も続いている」ということになります。
今回の場合、2回目のforeachでも $value に代入しているので、$array[3]が書き変わってしまっていたのが原因でした。
配列が壊れる過程
2回目のforeachで $array[3]の身に何が起こったのか、順を追って整理しました。
ループの順番 | foreach ($array as $value)で起こること | $array[3]の値 |
---|---|---|
array[0] の番 | $value ( = array[3] = 400 ) に、array[0] ( = 100 ) を代入 | 400 から 100 に変わる |
array[1] の番 | $value ( = array[3] = 100 ) に、array[1] ( = 200 ) を代入 | 100 から 200 に変わる |
array[2] の番 | $value ( = array[3] = 200 ) に、array[2] ( = 300 ) を代入 | 200 から 300 に変わる |
array[3] の番 | $value ( = array[3] = 300 ) に、array[3] ( = 300 ) を代入 | 300のまま |
400が入っていると思っていた$array[3]の値が次々に書き変わり、最終的に、直前の要素が入っていたということがよく理解できました。
対策①
対策を調べると、たくさんの人にunset($value)すればいいんだよと言われます。
<?php
$array = array(10, 20, 30, 40);
// (1)
foreach ($array as &$value) {
$value *= 10;
}
unset($value);
// (2)
foreach ($array as $value) {
var_dump($value);
}
たしかにこれで解決できます。
しかし、unsetを書き忘れる危険性があります。
複数名でコードをメンテナンスするとなると、なおさらです。
対策②
そもそもforeachで参照渡しをしなければ起こらないバグなので、
参照渡しにするのではなく、$arrayを書き換えるという方法をとりました。
<?php
$array = array(10, 20, 30, 40);
// (1)
foreach ($array as $key => $value) {
$array[$key] *= 10;
}
// (2)
foreach ($array as $value) {
var_dump($value);
}
対策②のほうが安心できます。
foreachでの参照渡しは、必要でなければ使わないほうがよいと思いました。
参考:https://qiita.com/buntafujikawa/items/f192d724a3c714f39c45