🤖

PHPでforeachの参照渡しをする時はunsetをセットで書く(もしくは使わない)

2024/12/15に公開2

はじめに

以前レビュー中にforeachの参照渡しが書かれているコードを読んでいて
動かしてみたら想定外の挙動をしたので備忘録として残します。

クイズ

$prime_numberの値は最終的には何になるでしょうか?

<?php
$prime_number = array(2,3,5,7);

foreach ($prime_number as &$value) {
    $value = $value * 2;
}

foreach ($prime_number as $key => $value) {
    echo "キー:" .$key ."\n";
    echo "値:" .$value ."\n";
}
print_r($prime_number);

予想

最初のforeachで参照渡しをしているので
$prime_number = array(4,6,10,14);になる。

2つ目のforeachechoしているだけなので$prime_numberの値は変わらない。
echoは当然

キー:0
値:4
キー:1
値:6
キー:2
値:10
キー:3
値:14

を返すだけのシンプルなロジック

答え

キー:0
値:4
キー:1
値:6
キー:2
値:10
キー:3
値:10   //あれ・・・
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 10  //こんなはずでは・・・
)

考察

公式リファレンスにももちろん書いているのですが
foreachで参照渡しを使った時
1つ目のforeachの最後のループが終わった後
$value$prime_number[3]を指したままになります。
ですので2つ目のforeachの処理1周毎に$prime_number[3]の値は4,6,10,10と上書きされます。

分かりやすいように各所で値を出してみます。

<?php
$prime_number = array(2,3,5,7);

foreach ($prime_number as &$value) {
    $value = $value * 2;
}
echo '1回目のforeachの後の$prime_number'."\n";
print_r($prime_number);

echo '↓から2回目のforeach'."\n";
foreach ($prime_number as $key => $value) {
    echo "キー:" .$key ."\n";
    echo "値:" .$value ."\n";
    print_r($prime_number);
}
echo '最終的な$prime_number'."\n";
print_r($prime_number);

結果

1回目のforeachの後の$prime_number
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 14
)
↓から2回目のforeach
キー:0
値:4
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 4
)
キー:1
値:6
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 6
)
キー:2
値:10
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 10
)
キー:3
値:10
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 10
)
最終的な$prime_number
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 10
)

修正方法

期待する結果を得るためには
1回目のforeachの後にunsetをすればいいので

<?php
$prime_number = array(2,3,5,7);

foreach ($prime_number as &$value) {
    $value = $value * 2;
}
unset($value); //ここだけ追加した
echo '1回目のforeachの後の$prime_number'."\n";
print_r($prime_number);

echo '↓から2回目のforeach'."\n";
foreach ($prime_number as $key => $value) {
    echo "キー:" .$key ."\n";
    echo "値:" .$value ."\n";
    print_r($prime_number);
}
echo '最終的な$prime_number'."\n";
print_r($prime_number);

結果

1回目のforeachの後の$prime_number
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 14
)
↓から2回目のforeach
キー:0
値:4
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 14
)
キー:1
値:6
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 14
)
キー:2
値:10
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 14
)
キー:3
値:14
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 14
)
最終的な$prime_number
Array
(
    [0] => 4
    [1] => 6
    [2] => 10
    [3] => 14
)

無事に$prime_number = array(4,6,10,14);になりました。

ちなみに参照渡しがトリッキーだと思うので、参照渡しを使わずに書くとこんな感じです。

<?php
$prime_number = array(2,3,5,7);

foreach ($prime_number as $key => $value) {
    $value = $value * 2;
    $prime_number[$key] = $value;
}
echo '1回目のforeachの後の$prime_number'."\n";
print_r($prime_number);

echo '↓から2回目のforeach'."\n";
foreach ($prime_number as $key => $value) {
    echo "キー:" .$key ."\n";
    echo "値:" .$value ."\n";
    print_r($prime_number);
}
echo '最終的な$prime_number'."\n";
print_r($prime_number);

速度検証

参照渡しを使う場合と使わない場合
同じロジックにしたらどちらが早いのか手元で実験してみました。
100万個の配列を作って各要素を2倍する時間を10回ずつ測定

参照渡し有り

<?php
$array=array();

for ($i = 1; $i <= 1000000; $i++) {
    $array[$i] = $i;
}

$time_start = microtime(true);

foreach ($array as &$value) {
    $value = $value * 2;
}

$time = microtime(true) - $time_start;
$time = round($time, 4);
echo $time ."秒";

10回実行した平均:0.02236秒

参照渡し無し

<?php
$array=array();

for ($i = 1; $i <= 1000000; $i++) {
    $array[$i] = $i;
}

$time_start = microtime(true);

foreach ($array as $key => $value) {
    $value = $value * 2;
    $array[$key] = $value;
}

$time = microtime(true) - $time_start;
$time = round($time, 4);
echo $time ."秒";

10回実行した平均:0.02027秒

まとめ

若干ではあるが参照渡しを使わない方が早かったです(参照渡し[有り]0.02236秒 [無し]0.02027秒)
参照渡しを使う方がキーの値をforeachで指定せずとも配列の値を書き換えられるが
unsetを忘れると不具合の原因になりかねないので複数人で読むようなプロダクトには向いていないかもしれないです。

ソースの量を減らすことも大切ですが、誰にでも読みやすいコードだ可読性が上がっていいかもしれませんね。

NE株式会社の開発ブログ

Discussion

naoyuki42naoyuki42

個人的に参照渡しや値渡しにあまり馴染みがなかったので勉強になりました!

参照渡し無しのやり方として、別の変数に格納するやり方も自分は良く使っている気がします

<?php
$prime_number = array(2,3,5,7);
$result_array = array();

foreach ($prime_number as $value) {
    $result_array[] = $value * 2;
}
jun_ibarakijun_ibaraki

コメントありがとうございます!!!
確かに、別の変数に格納する方が一般的な気がします。
こちらこそ勉強になりました!