5

この記事は最終更新日から1年以上が経過しています。

PHP foreach 参照渡し 罠

データの出力処理で、最後の行が出力されず、代わりに最後から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

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
新規登録
すでにアカウントを持っている方はログイン
5