PHP Foreach参照渡し:最後の要素の複製?(バグ?)


159

私が書いている単純なphpスクリプトで、非常に奇妙な動作をしました。バグを再現するのに必要な最低限に減らしました:

<?php

$arr = array("foo",
             "bar",
             "baz");

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

?>

これは出力します:

Array
(
    [0] => foo
    [1] => bar
    [2] => baz
)
Array
(
    [0] => foo
    [1] => bar
    [2] => bar
)

これはバグか、実際に発生すると思われる奇妙な動作ですか?


もう一度値で行います。3回目に変更されるかどうかを確認します...?
Shackrock

1
@Shackrock、それは値による繰り返しループでもう変化しないようです。
地域性

1
興味深いことに、2番目のループを$ item以外のものを使用するように変更すると、期待どおりに機能します。
スティーブクラリッジ

9
ループ本体の最後で常に項目の設定を解除しますforeach($x AS &$y){ ... unset($y); }。-間違いが多いため、実際にはphp.netにあります(場所はわかりません)。
ルディー

回答:


170

最初のforeachループの後$itemも、によって使用されている値への参照$arr[2]です。したがって、参照によって呼び出されない2番目のループの各foreach呼び出しは、その値、つまり$arr[2]を新しい値に置き換えます。

したがって、ループ1、値、およびに$arr[2]なり$arr[0]ます。これは「foo」です。
ループ2、値と$arr[2]なる$arr[1]、それは「バー」です。
ループ3、値andに$arr[2]なる$arr[2]、 'bar'(ループ2のため)。

値「baz」は、2番目のforeachループの最初の呼び出しで実際に失われます。

出力のデバッグ

ループの反復ごとに、の値をエコーし$item、配列を再帰的に出力します$arr

最初のループが実行されると、次の出力が表示されます。

foo
Array ( [0] => foo [1] => bar [2] => baz )

bar
Array ( [0] => foo [1] => bar [2] => baz )

baz
Array ( [0] => foo [1] => bar [2] => baz )

ループの最後で、$itemはまだと同じ場所を指してい$arr[2]ます。

2番目のループが実行されると、次の出力が表示されます。

foo
Array ( [0] => foo [1] => bar [2] => foo )

bar
Array ( [0] => foo [1] => bar [2] => bar )

bar
Array ( [0] => foo [1] => bar [2] => bar )

配列が新しい値をに入れるたびに$item$arr[3]同じ値で更新されることに気づくでしょう。どちらもまだ同じ場所を指しているためです。ループが配列の3番目の値にbar到達すると、そのループの直前の反復によって設定されただけなので、ループには値が含まれます。

バグですか?

いいえ。これは参照アイテムの動作であり、バグではありません。それは次のようなものを実行することに似ています:

for ($i = 0; $i < count($arr); $i++) { $item = $arr[$i]; }

foreachループは、参照されるアイテムを無視できるという点で、特別なものではありません。これは、ループの外側と同じように、その変数を毎回新しい値に設定するだけです。


4
少し注意深く修正しました。$itemはへの参照ではありません。に$arr[2]含まれる値$arr[2]はが参照する値への参照$itemです。違いを説明するために、を設定解除することもでき$arr[2]$item影響を受けず、への書き込み$itemは影響しません。
ポールビガー

2
この動作を理解するのは複雑であり、問​​題を引き起こす可能性があります。私はこれを私のお気に入りの1つとして、なぜ「参照」することを(できる限り)回避すべきかを生徒に示しています。
Olivier Pons

1
$itemforeachループが終了したときに範囲外にならないのはなぜですか?これは閉鎖の問題のようですか?
jocull

6
@jocull:PHPでは、foreach、for、whileなどは独自のスコープを作成しません。
アニムソン

1
@ jocull、PHPにはローカル変数(ブロック)がありません。それが私を困らせる理由の一つ。
Qtax

29

$itemは、$arr[2]animusonが指摘したように、2番目のforeachループへの参照であり、2番目のforeachループによって上書きされます。

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

unset($item); // This will fix the issue.

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

3

これは正式にはバグではないかもしれませんが、私の意見ではバグです。ここでの問題は$item、他の多くのプログラミング言語でそうであるように、ループが終了したときにスコープの外に出ることが期待されていることだと思います。しかし、そうではないようです...

このコード...

$arr = array('one', 'two', 'three');
foreach($arr as $item){
    echo "$item\n";
}    
echo $item;

出力を与えます...

one
two
three
three

他の人々がすでに言ったように、あなたは$arr[2]2番目のループで参照された変数を上書きしていますが、それ$itemは範囲外になったことがないので起こっているだけです。皆さんはどう思いますか...バグ?


4
1)バグではありません。これはマニュアルですでに呼び出されており、意図したとおりに多くのバグレポートで却下されています。2)質問の答えにはなりません...
BoltClock

スコープの問題のせいではなく、$ itemが最初のforeachの後に残っていると予想していましたが、foreachが変数をREPLACINGするのではなく更新することに気付きませんでした。たとえば、2番目のループの前にunset($ item)を実行するのと同じです。unsetは値(つまり配列の最後の要素)をクリアしないことに注意してください。単に変数を削除します。
Programster

残念ながら、PHPは{}一般的にループやブロックの新しいスコープを作成しません。これが言語の仕組みです
ファビアンシュメングラー2015


0

PHPの正しい動作は、私の意見では通知エラーになるはずです。foreachループで作成された参照変数がループの外で使用された場合、通知が発生するはずです。この動作に陥るのは非常に簡単で、発生したときにそれを見つけるのは非常に困難です。また、foreachのドキュメントページを読む開発者はいません。これは助けにはなりません。

unset()この種の問題を回避するには、ループの後に参照する必要があります。参照のunset()は、元のデータを損なうことなく参照を削除します。


0

これは、refディレクティブ(&)を使用しているためです。最後の値は2番目のループに置き換えられ、配列が壊れます。最も簡単な解決策は、2番目のループに別の名前を使用することです。

foreach ($arr as &$item) { ... }

foreach ($arr as $anotherItem) { ... }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.