【PHP】foreachで配列を操作したのに何も変わらなかった時は値渡しと参照渡しを理解すべし

エディタPHP

こんにちは。いっとくです!

プログラミングってすごくいろんなことができるのでめちゃくちゃ覚えることが多いな〜と思いつつも、よく使うものもあれば全然使わないものもあります。

中でも、トップクラスでよく使うのが配列操作ってやつです!

配列の中身をちょっと変えたり構造を変えたりすることで、DBへの登録や画面に表示するためのつじつま合わせを行うみたいな処理なんかはよく使いますね。

配列操作はあまりにもよく使うので、フレームワークを使って開発している場合は、フレームワーク自体に便利な配列操作のメソッドが用意されていることもあります。

とは言え、必ずしも良いされているものだけで満足できるというわけでもないので、時には自分で行うこともありますよね。

ということで今回は配列操作に関わる話で、僕がエンジニアになりたての頃にぶつかった内容について解説していきたいと思います。

foreachを使って配列の中身を操作したはずのに、全然元の変数が変わらなかったという問題です!

スポンサーリンク

foreachに値を渡す時、元の配列は実はコピーされている

当時の僕が遭遇したのが以下のような状況でした

<?php

// 全部の文字列にSuzukiをつけたい!
$names = array('Takashi', 'Michiko', 'Michel');

// よしforeachで一個ずつ処理しよう
foreach ($names as $name) {
    // とりあえず、一個一個に文字を足せば大丈夫、、、だよね?
    $name .= 'Suzuki';
}

var_dump($names); // "Takashi", "Michiko", "Michel"
// 全然変わってないじゃん!

foreachで$namesの値を$nameとして取り出し、それぞれの$nameに共通の操作を加えれば、元の配列も変わるでしょと思って全力でforeachを回したのですが、元の配列には傷一つ与えることすら叶わなかったのです。

この原因はforeachを使う際に作ったas句以下の変数が元の配列とは完全に別物として作られていることが原因です。

なので解決するために、まずは参照渡しと値渡しというものについて理解する必要があります。

値渡しと参照渡しとは?

なんだか小難しい単語が出てきましたが、これは簡単に言うと、値を渡す時にコピーして渡すか本物を渡すかという違いです。

そして、このコピーを渡す方が値渡しで、本物を渡す方が参照渡しと言われています。

厳密には本物を渡していると言うよりは、原本の方を見にいっているみたいな感じなのですが、どういう動きをするのかが思うので、ざっくり理解していただければと思います。

PHPではデフォルトでは値渡しが行われています。

なので参照渡しをする場合は&を使ってあげましょう。

変数に代入する際、参照渡しをする場合は、以下のコードのような具合で=の後ろに&をつけます。

<?php

$a = 1;
$b = $a; // 値渡し
$c =& $a; // 参照渡し

$a++;

echo $a; // 2
echo $b; // 1
echo $c; // 2

$c++;

echo $a; //3

$bも$cも$aを代入しただけなのに、ちょっと挙動が変わりましたね。

代入し終わった後に$aだけに1を足したに、$cにも1が足されていますね〜。

これが参照渡しと言われるもので、$cは常に$aの中身を参照するように渡されているわけです。

さらに$cに1を足すと、$aの値も3に増えています…

もはや完全に運命共同体。こいつらは値を共有するようになったわけです。

参照渡しを使ったのに、うっかりそのことを忘れて同じ変数名を使ってしまうと、思わぬところでバグを生み出しそうな動きですね〜。

そして、foreachで配列を操作した時に元の値が変化していないのは、実はforeachを使う際に作った$nameは値渡しで作られているからなのです。

値渡しで作った配列は独立しているので、毎回のループ処理で変更は加えられているのですが、結局元の配列$namesの値には影響を及ぼしません。

変数を代入した例のところでいう$bみたいな存在です

なので、もしforeachを使って元の配列の値一個一個を一括で変更したいのであれば、以下のような形で参照渡しをする必要があります。

<?php

// 全部の文字列にSuzukiをつけたい!
$names = array('Takashi', 'Michiko', 'Michel');

// よし参照渡しをしよう
foreach ($names as &$name) {
    $name .= 'Suzuki';
}
// unsetしましょう(理由は本文で解説)
unset($name);

var_dump($names); // "TakashiSuzuki", "MichikoSuzuki", "MichelSuzuki"
// 無事変わったぜ

as句の後ろの変数に&をつけてあげることで参照渡しになるので、元の配列を操作することができるようになります。

おっと、そういえば変なのが増えてますね…

そうです。unsetという関数が増えました。

このunsetが何をしているかというと、$nameを作る際に渡された参照を解除してます。

実はこの参照の解除を忘れると、思わぬバグの原因になりかねないのでに気をつけましょう。

foreachを参照渡しした際の困った挙動

前述したように、参照渡しは元の変数と値を共有しています。

僕ははじめこれを聞いた時、「まぁ、別に放っておいてもいいかなと?」いう感じだったのですが、ここでunsetをして参照を解除しないとこんなことが起きるんです。

<?php

// 全部の文字列にSuzukiをつけたい!
$names = array('Takashi', 'Michiko', 'Michel');

// よし参照渡しをしよう
foreach ($names as &$name) {
    $name .= 'Suzuki';
}

$name = 'Tanaka';

var_dump($names); // "TakashiSuzuki", "MichikoSuzuki", "Tanaka"
// 無事変わったぜ…って、ん?…最後がTanakaになってる!!?

上記の例では基本的にやっていることは今までの例とほとんど変わらないのですが、unsetをせずに別の行で$nameにTanakaを代入しています。

すると、配列の一番最後がTanakaになっている!!おい!Tanaka!

これはforeachが終わった後も$nameの変数が残っているからなのですが、$nameが引き続き参照をしてしまっているため、後で代入したTanakaがforeachの一番最後の要素に反映されてしまったんですね、、、

つまりforeachの後にunsetを忘れてしまい、たまたま同じ名前の変数なんかを使っちゃうと、参照が発動して、せっかく操作して整えた配列が汚染されてしまうわけです!!

なので、foreachで参照渡しを使った後は必ずunsetをするのを忘れないようにしましょう!

ちなみに参照渡しよくわからなくて怖いっすって人は、別の方法として全く別の配列を作っちゃうっていうのもありですね。

<?php

// 全部の文字列にSuzukiをつけたい!
$names = array('Takashi', 'Michiko', 'Michel');

// ということで別の配列に突っ込む
$addSuzukiNames = array();
foreach ($names as $name) {
    $addSuzukiNames[] = $name . 'Suzuki';
}

var_dump($addSuzukiNames); // "TakashiSuzuki", "MichikoSuzuki", "MichelSuzuki"

これだと新しく作る配列に名前をつけることができるので、trimmedとかaddSlashesとかどんな配列操作を行ったかがわかるようにすることができるというメリットもあります。

しかし配列操作をするたびに新しく配列を作ることになるので、使う頻度によっては配列が大量生産されてしまって、コードがわかりにくくなるというデメリットもあるので、バランスをみて使い分けていきたいところですね!!

以上、foreachで一個一個の要素を操作しても、元の配列が変わらない原因は、値渡しになっているからだよというお話でした!

もし、この内容にご意見や訂正とうあれば、気軽にコメントいただけると嬉しいです!

では!

コメント

タイトルとURLをコピーしました