LoginSignup
6
6

More than 3 years have passed since last update.

データの出力処理で、最後の行が出力されず、代わりに最後から2番目の行が2回出力されるというバグが起こりました。
調べてみると、foreachで陥りがちなバグであることがわかったので、対応して得たことをまとめました。

事象

$arrayの要素を、すべて10倍したいとします。
配列の値をforeachの中で変更したいとき、
変数の前に「&」を付けることで、参照渡しで値を設定することができます。(1)
そのあとに、出力処理のために再度foreachしました。(2)

bug.php
<?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を抜けたところの配列の中身を見てみると、

test.php
<?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)すればいいんだよと言われます。

unset.php
<?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を書き換えるという方法をとりました。

key.php
<?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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6