PHPで配列を扱う際、foreach
ループは非常に便利で、日常的に使われる構文の一つではないでしょうか。
しかし、ループ処理中に配列の要素そのものの値を変更しようとして、うまくいかなかった経験を持つ方もいるかもしれません。
foreach
の中で配列の値を書き換えたいのに、なぜか元の配列が変わらない…」その原因は、foreach
のデフォルトの動作である「値渡し」にあります。
そして、この問題を解決する鍵が「参照渡し」という仕組みなのです。
この記事では、PHP(PHP 8.x以降)を基準として、foreach
ループにおける参照渡しの使い方を基礎から徹底的に解説していきます。
【本記事の信頼性】
- 執筆者は元エンジニア
- 大手プログラミングスクールのWebディレクター兼ライターを経験
- 自らも地元密着型のプログラミングスクールを運営
受講生から評判の良いプログラミングスクール
スクール |
特徴 |
受講料金 |
大手比較サイトで4年連続人気NO.1!受講生からの評判も非常に高く、Web系のエンジニアを目指すならRUNTEQ一択。 | 550,000円(給付金適用あり) | |
月単価80万円以上の現役エンジニア講師による指導!一度入会すればサポートは半永久的。 | 498,000円 | |
格安で質の高いWeb制作スキルを習得したい人におすすめ!業界最安級の料金でありながら、コミュニティやサポートが充実。 | 129,800円~ | |
完全無料でプログラミングが学べる貴重なスクール!最短1ヶ月で卒業可能。ゼロスク運営会社への就職もできる。 | 無料 | |
長期間に渡って学習し、希少人材を目指す人に最適なスクール!受講料は高いものの、高収入を得られる人材を目指せる。 | 96~132万円 |
PHPのforeachにおける「値渡し」と「参照渡し」の違い
まず、foreach
の基本的な動作である「値渡し」と、本題である「参照渡し」の違いについて理解を深めることが重要です。
この二つの違いを知ることで、なぜ参照渡しが必要になるのかが明確になります。
基本的な動作「値渡し」
通常、何も意識せずにforeach
ループを使うと、それは「値渡し」として動作します。
値渡しとは、ループの各イテレーションで、配列の要素の「コピー」がループ変数(例:$value
)に代入される仕組みを指します。
つまり、ループ内で操作しているのは、あくまで元の配列の要素の複製であり、元の要素そのものではありません。
そのため、ループ変数に対して値を変更する操作を行っても、元の配列には何の影響も与えないのです。
実際のコードで確認してみましょう。
サンプルコード
<?php
// 数値の配列を用意
$numbers = [1, 2, 3, 4, 5];
echo "ループ処理前の配列:\n";
print_r($numbers);
// foreachで各要素を2倍にしようと試みる(値渡し)
foreach ($numbers as $value) {
$value = $value * 2;
}
echo "\nループ処理後の配列:\n";
print_r($numbers);
実行結果
ループ処理前の配列:
Array
(
[0] => 1
[1] => 2
[2] => 3
[3] => 4
[4] => 5
)
ループ処理後の配列:
Array
(
[0] => 1
[1] => 2
[2] => 3
[3] => 4
[4] => 5
)
コードの解説
このコードでは、配列$numbers
の各要素を2倍にしようとしています。
しかし、実行結果を見ると、ループ処理後も配列$numbers
の中身は一切変わっていません。
これは、foreach ($numbers as $value)
の部分が値渡しで動作しているためです。
ループ内の$value
は$numbers
の各要素のコピーなので、$value = $value * 2;
という処理は、そのコピーした値に対して行われているに過ぎません。
元の配列$numbers
には手が届いていない、というわけです。
配列の値を直接変更する「参照渡し」
次に、配列の値をループ内で直接変更したい場合に用いる「参照渡し」について見ていきましょう。
参照渡しを利用するには、ループ変数の前にアンパサンド(&
)を付けます。
foreach ($array as &$value)
このように記述することで、ループ変数$value
は、配列要素のコピーではなく、配列要素そのものを指し示すようになります。
これを「参照」と呼びます。
$value
が元の配列要素への参照を持つため、ループ内で$value
を変更すると、それは直接元の配列要素を変更する操作となるのです。
先ほどのコードを参照渡しで書き換えてみましょう。
サンプルコード
<?php
// 数値の配列を用意
$numbers = [1, 2, 3, 4, 5];
echo "ループ処理前の配列:\n";
print_r($numbers);
// foreachで各要素を2倍にする(参照渡し)
// ループ変数の前に & を付ける
foreach ($numbers as &$value) {
$value = $value * 2;
}
echo "\nループ処理後の配列:\n";
print_r($numbers);
実行結果
ループ処理前の配列:
Array
(
[0] => 1
[1] => 2
[2] => 3
[3] => 4
[4] => 5
)
ループ処理後の配列:
Array
(
[0] => 2
[1] => 4
[2] => 6
[3] => 8
[4] => 10
)
コードの解説
foreach ($numbers as &$value)
のように、$value
の前に&
を付けただけです。
しかし、実行結果は劇的に変わりました。
ループ処理後の配列$numbers
の各要素が、意図通り2倍になっているのが確認できます。
&$value
とすることで、$value
は$numbers[0]
、$numbers[1]
、$numbers[2]
…といった元の配列の各要素そのものを順番に指し示すようになります。
そのため、$value = $value * 2;
という代入が、元の配列を直接書き換える結果につながるのです。
foreachで参照渡しを使う具体的なサンプルコード
参照渡しの基本がわかったところで、より実践的なシナリオに沿ったサンプルコードをいくつか紹介します。
数値配列の各要素に消費税を加算する
シンプルな数値配列を使い、各価格に10%の消費税を加算する例です。
サンプルコード
<?php
$prices = [100, 250, 800, 1200];
$taxRate = 0.10;
// 参照渡しを使い、各価格に税込み価格を再代入する
foreach ($prices as &$price) {
$price = $price + ($price * $taxRate);
// PHP 8.0以降では、より簡潔に書くこともできます
// $price += $price * $taxRate;
}
echo "税込み価格リスト:\n";
print_r($prices);
実行結果
税込み価格リスト:
Array
(
[0] => 110
[1] => 275
[2] => 880
[3] => 1320
)
コードの解説
foreach ($prices as &$price)
によって、ループ変数$price
は配列$prices
の各要素を直接参照します。
ループ内で計算された税込み価格が$price
に代入されるたびに、元の$prices
配列の値が更新されていきます。
連想配列の値を条件に応じて更新する
ユーザー情報のリストがあり、特定の条件に合致するユーザーのステータスを更新する、といったシナリオはよくあります。
サンプルコード
<?php
$users = [
['id' => 1, 'name' => '田中', 'status' => 'active'],
['id' => 2, 'name' => '鈴木', 'status' => 'inactive'],
['id' => 3, 'name' => '佐藤', 'status' => 'active'],
['id' => 4, 'name' => '高橋', 'status' => 'inactive'],
];
// 参照渡しで各ユーザー情報を取得
foreach ($users as &$user) {
// ステータスが 'inactive' の場合に 'pending' へ変更する
if ($user['status'] === 'inactive') {
$user['status'] = 'pending';
}
}
echo "更新後のユーザーリスト:\n";
print_r($users);
実行結果
更新後のユーザーリスト:
Array
(
[0] => Array
(
[id] => 1
[name] => 田中
[status] => active
)
[1] => Array
(
[id] => 2
[name] => 鈴木
[status] => pending
)
[2] => Array
(
[id] => 3
[name] => 佐藤
[status] => active
)
[3] => Array
(
[id] => 4
[name] => 高橋
[status] => pending
)
)
コードの解説
この例では、$users
という多次元配列を扱っています。
ループ変数$user
は、$users[0]
、$users[1]
といった内部の連想配列そのものを参照します。
if
文の条件に合致した場合、$user['status'] = 'pending';
という処理が実行されます。
これは元の配列のstatus
キーの値を直接書き換えているため、ループ終了後にはinactive
だったユーザーのステータスがpending
に更新されていることがわかります。
foreach参照渡しの最大の注意点と回避策
参照渡しは非常に強力な機能ですが、一つだけ絶対に知っておかなければならない重要な注意点が存在します。
これを怠ると、予期せぬバグの原因となり、デバッグに多くの時間を費やすことになりかねません。
ループ後に残る参照の危険性
foreach
の参照渡しループが終了した後も、ループ変数(&$value
)は、配列の最後の要素への参照を保持し続けます。
この事実に気づかずに、後続の処理で同じ変数名を使ってしまうと、意図せず配列の最後の要素を書き換えてしまう可能性があるのです。
非常に危険な挙動を、実際のコードで見てみましょう。
サンプルコード
<?php
$numbers = [1, 2, 3];
// 参照渡しでループ
foreach ($numbers as &$value) {
// 何らかの処理
}
echo "ループ直後の配列:\n";
print_r($numbers);
// ループは終わったが、$value は $numbers[2] (最後の要素) を参照し続けている
// 別の処理で、うっかり同じ変数名 $value を使ってしまった
$value = 100;
echo "\n変数を再利用した後の配列:\n";
print_r($numbers);
実行結果
ループ直後の配列:
Array
(
[0] => 1
[1] => 2
[2] => 3
)
変数を再利用した後の配列:
Array
(
[0] => 1
[1] => 2
[2] => 100
)
コードの解説
foreach
ループが終わった時点では、配列は[1, 2, 3]
のままです。
しかし、ループ変数の$value
は、配列$numbers
の最後の要素、つまり$numbers[2]
への参照を保持しています。
その後の$value = 100;
という一見無関係に見える代入処理が、実は$numbers[2] = 100;
と同じ意味になってしまいます。
結果として、配列の最後の要素が3
から100
に書き換わってしまいました。これは多くの場合、プログラマが意図しない副作用でしょう。
unset()による参照の解除
この危険な挙動を防ぐための最も確実で推奨される方法が、ループの直後でunset()
関数を使ってループ変数の参照を解除することです。
サンプルコード
<?php
$numbers = [1, 2, 3];
// 参照渡しでループ
foreach ($numbers as &$value) {
// 何らかの処理
}
// ループの直後で unset を呼び出し、参照を解除する
unset($value);
echo "unset() 実行後の配列:\n";
print_r($numbers);
// 同じ変数名を使っても、もう元の配列には影響しない
$value = 100;
echo "\n変数を再利用した後の配列:\n";
print_r($numbers);
echo "\n変数 \$value の中身:\n";
var_dump($value);
実行結果
unset() 実行後の配列:
Array
(
[0] => 1
[1] => 2
[2] => 3
)
変数を再利用した後の配列:
Array
(
[0] => 1
[1] => 2
[2] => 3
)
変数 $value の中身:
int(100)
コードの解説
foreach
ループのすぐ後にunset($value);
を追加しました。
これにより、$value
と$numbers
の最後の要素との間の参照リンクが断ち切られます。
その後で$value = 100;
を実行しても、それは単に新しい変数$value
に100
を代入するだけの通常の処理となり、配列$numbers
には何の影響も与えません。
foreach
で参照渡しを使った際は、ループの直後にunset()
で変数を後始末する。
これを一つのルールとして徹底することで、未来のバグを未然に防ぐことができるのです。
参照渡しに関するよくある質問 (FAQ)
最後に、参照渡しに関してよく寄せられる質問について解説します。
参照渡しはパフォーマンスに影響しますか?
結論から言うと、現代のPHP(PHP 8.x以降)において、パフォーマンス向上のために参照渡しを選択するメリットはほとんどありません。
かつて、非常に巨大な配列を扱う際に、値渡しによるデータコピーのオーバーヘッドを避けるために参照渡しが使われることがありました。
しかし、現在のPHPエンジンは「Copy-on-Write」という賢い仕組みを採用しています。
これは、値渡しであっても、データが実際に変更されるまでは内部的なコピーを作成しないというものです。
そのため、ループ内で値を読み取るだけの場合、値渡しでも参照渡しでもパフォーマンスに大きな差は生まれません。
参照渡しを使うべき主な理由は、あくまで「ループ内で元の配列の値を変更したい場合」に限られます。
パフォーマンスチューニングの手段として考えるのは適切ではないでしょう。
オブジェクトの配列をforeachで扱う場合はどうなりますか?
PHPにおいて、オブジェクトの扱いは少し特殊です。
変数がオブジェクトを保持している場合、その変数はオブジェクトそのものではなく、オブジェクトを指し示す識別子(ハンドル)を保持しています。
そのため、オブジェクトを別の変数に代入したり、関数に渡したりする際、デフォルトで参照のように振る舞います。
オブジェクトのプロパティを変更する場合、&
を使った参照渡しは必ずしも必要ありません。
サンプルコード
<?php
// stdClassオブジェクトを作成する簡単な方法
$user1 = (object)['name' => '山田', 'points' => 100];
$user2 = (object)['name' => '加藤', 'points' => 200];
$users = [$user1, $user2];
// 通常の値渡しループ
foreach ($users as $user) {
// オブジェクトのプロパティを変更する
$user->points += 50;
}
echo "ポイント加算後のユーザーリスト:\n";
print_r($users);
実行結果
ポイント加算後のユーザーリスト:
Array
(
[0] => stdClass Object
(
[name] => 山田
[points] => 150
)
[1] => stdClass Object
(
[name] => 加藤
[points] => 250
)
)
コードの解説
この例のように、&
を使わなくても、オブジェクトのプロパティ($user->points
)を直接変更できています。
ただし、ループ変数$user
自体を、全く別の新しいオブジェクトに差し替えたい、といった特殊なケースでは参照渡しが必要になります。
しかし、ほとんどの場合はプロパティの変更で事足りるため、オブジェクトの配列に対しては&
を付けずにforeach
を使うのが一般的です。
まとめ
今回は、PHPのforeach
ループにおける参照渡しについて、その仕組みから具体的な使い方、そして最も重要な注意点までを詳しく解説しました。
なお、PHPを体系的に学んだり、PHPのスキルを高めたりするためには、プログラミングスクールを利用するのも有効です。
細かな疑問がすぐに解決するだけでなく、現役エンジニアが「質の高いポートフォリオ」を作成するための手助けをしてくれたり、エンジニア就職・転職のコツを教えてくれたりするなど、様々なメリットがありますので、独学に疲れた方は検討してみてはいかがでしょうか。