PHPのforeachで参照渡しをしたい時の注意点

採用はこちら

PHPのforeachでは、値変数の前に&を付けることで、配列の各要素を参照として扱えます。

$array = [1, 2, 3];

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

print_r($array);

実行結果は次のようになります。

Array
(
    [0] => 2
    [1] => 4
    [2] => 6
)

通常のforeachでは、ループ変数を書き換えても元の配列要素は変更されません。

しかし、foreach ($array as &$value)のように&を付けると、$valueは配列要素への参照になります。

そのため、ループ内で$valueを変更すると、元の配列の中身も直接変更されます。

ただし、参照付きforeachには注意点があります。特に重要なのは、ループ終了後もループ変数が最後の要素を参照し続けるという点です。

この性質を理解していないと、配列の最後の要素が意図せず書き換わるバグにつながります。

目次

foreachで参照を使う基本形

通常のforeachでは元の配列は変更されない

まず、通常のforeachの動きを確認します。

$array = [1, 2, 3];

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

print_r($array);

実行結果は次の通りです。

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)

$valueを2倍にしていますが、元の$arrayは変わっていません。

これは、通常のforeach ($array as $value)では、$valueを変更しても元の配列要素には反映されないためです。

厳密には、PHP内部で毎回必ず実体コピーが発生しているという意味ではありません。

PHPにはコピーオンライトの仕組みがあるため、内部的なメモリ管理はもう少し複雑です。

ただ、コードを書く側の理解としては、通常のforeachでは「ループ変数を変更しても元の配列は変わらない」と覚えておけば問題ありません。

参照付きforeachでは元の配列を直接変更できる

元の配列を直接変更したい場合は、値変数の前に&を付けます。

$array = [1, 2, 3];

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

print_r($array);

実行結果は次のようになります。

Array
(
    [0] => 2
    [1] => 4
    [2] => 6
)

foreach ($array as &$value)と書くことで、$valueは配列要素への参照になります。

そのため、

$value *= 2;

と書くと、$valueだけでなく、元の$arrayの要素も変更されます。

参照付きforeachの最大の注意点

ループ後も最後の要素への参照が残る

参照付きforeachで最も注意すべき点は、ループが終わった後も、ループ変数が配列の最後の要素を参照したままになることです。

例えば、次のコードを見てください。

$array = [1, 2, 3, 4];

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

// unset($value); をしていない

print_r($array);

この時点では、配列は次のように変更されています。

Array
(
    [0] => 2
    [1] => 4
    [2] => 6
    [3] => 8
)

ここまでは問題ありません。

しかし、foreachが終わった後も、$value$array[3]、つまり最後の要素を参照したままです。

この状態で、後続処理でも同じ$valueを使うと、意図しない書き換えが起こる可能性があります。

unsetしないと最後の要素が書き換わる

次のコードは、参照付きforeachで配列を変更した後、もう一度通常のforeachで配列を回しています。

$array = [1, 2, 3, 4];

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

// unset($value); をしていない

foreach ($array as $value) {
    // 何もしない
}

print_r($array);

一見すると、最初のループで配列の値を2倍にし、2回目のループでは何もしていないように見えます。

期待する結果は次のような配列です。

Array
(
    [0] => 2
    [1] => 4
    [2] => 6
    [3] => 8
)

しかし、実際には次のようになります。

Array
(
    [0] => 2
    [1] => 4
    [2] => 6
    [3] => 6
)

最後の要素が8ではなく、6になっています。

原因は、最初のforeachの後も$valueが最後の要素への参照を持ち続けているためです。

2回目のforeachで、

foreach ($array as $value) {
}

と書くと、各ループで$valueに値を代入しているように見えます。

しかし、$valueはまだ$array[3]への参照です。そのため、実際には次のような処理が起こります。

$value = $array[0]; // 実質的には $array[3] = $array[0]
$value = $array[1]; // 実質的には $array[3] = $array[1]
$value = $array[2]; // 実質的には $array[3] = $array[2]
$value = $array[3]; // 実質的には $array[3] = $array[3]

その結果、最後の要素が途中で上書きされてしまいます。

このバグは見た目では原因が分かりにくく、実務でも非常に厄介です。

参照付きforeachの後はunsetする

unsetでループ変数を破棄する

参照付きforeachを使った後は、基本的に直後にunset()を書きます。

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

ここで重要なのは、unset($value)が配列の要素を削除する処理ではないという点です。

削除しているのは、ループ変数である$valueです。

unset($value);

によって、$valueという変数が現在のスコープから破棄されます。その結果、$valueに残っていた最後の配列要素への参照も切れます。

つまり、unset($value)は「配列を消す処理」ではなく、「最後の要素への参照が残らないように、ループ変数を破棄する処理」と考えると分かりやすいです。

安全な書き方

参照付きforeachを使う場合は、次の形を基本にすると安全です。

$array = [1, 2, 3, 4];

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

foreach ($array as $value) {
    echo $value . PHP_EOL;
}

unset($value)によって、最初のループで残っていた参照が切れています。

そのため、後続のforeachで同じ$valueという変数名を使っても、最後の要素が書き換わる問題は起きません。

ただし、保守性を考えるなら、後続のforeachでは別の変数名を使うのもおすすめです。

$array = [1, 2, 3, 4];

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

foreach ($array as $currentValue) {
    echo $currentValue . PHP_EOL;
}

unset()したうえで変数名も分けておくと、後からコードを読んだ人にも意図が伝わりやすくなります。

「参照渡し」という表現について

厳密には「参照付きforeach」が正確

PHPの記事では、foreach ($array as &$value)のことを「foreachの参照渡し」と表現することがあります。

実務上はこの言い方でも意味は通じます。

ただし、厳密には関数の引数として値を渡しているわけではないため、「参照渡し」よりも「参照付きforeach」や「参照による反復」と表現したほうが正確です。

例えば、以下のように表現できます。

foreachで値変数を参照にして回す

または、

参照付きforeachを使う

コードとしては、次の形です。

foreach ($array as &$value) {
    // $valueは配列要素への参照
}
unset($value);

実務で起こりやすいバグ例

商品データの加工後に一覧表示するケース

ECサイトや管理画面では、商品一覧の配列を加工してから表示する処理がよくあります。

例えば、商品価格に税込価格を追加するケースです。

$products = [
    ['name' => 'Apple', 'price' => 100],
    ['name' => 'Banana', 'price' => 200],
    ['name' => 'Orange', 'price' => 300],
];

foreach ($products as &$product) {
    $product['priceWithTax'] = (int) floor($product['price'] * 1.1);
}

// unset($product); を忘れている

foreach ($products as $product) {
    echo $product['name'] . ':' . $product['priceWithTax'] . PHP_EOL;
}

このコードは危険です。

最初のforeach$productは最後の要素、つまりOrangeの商品データを参照したままになります。

その後のforeachでも同じ$productを使っているため、最後の要素が意図せず書き換わる可能性があります。

正しくは、参照付きforeachの直後にunset($product)を書きます。

$products = [
    ['name' => 'Apple', 'price' => 100],
    ['name' => 'Banana', 'price' => 200],
    ['name' => 'Orange', 'price' => 300],
];

foreach ($products as &$product) {
    $product['priceWithTax'] = (int) floor($product['price'] * 1.1);
}
unset($product);

foreach ($products as $product) {
    echo $product['name'] . ':' . $product['priceWithTax'] . PHP_EOL;
}

このように書けば、最後の要素への参照が残らないため安全です。

ユーザー一覧にステータスを追加するケース

ユーザー一覧の配列を加工する場合も、参照付きforeachはよく使われます。

$users = [
    ['name' => 'Tanaka', 'active' => true],
    ['name' => 'Sato', 'active' => false],
];

foreach ($users as &$user) {
    $user['status'] = $user['active'] ? '有効' : '無効';
}
unset($user);

print_r($users);

実行結果は次のようになります。

Array
(
    [0] => Array
        (
            [name] => Tanaka
            [active] => 1
            [status] => 有効
        )

    [1] => Array
        (
            [name] => Sato
            [active] => 
            [status] => 無効
        )
)

このように、配列内の各要素に新しいキーを追加したい場合、参照付きforeachは便利です。

ただし、この場合もunset($user)は忘れないようにしましょう。

参照付きforeachを使うべきケース

元の配列を直接変更したい場合

参照付きforeachが向いているのは、元の配列を直接変更したい場合です。

例えば、文字列の前後の空白を削除する処理です。

$names = [' Tanaka ', ' Sato ', ' Suzuki '];

foreach ($names as &$name) {
    $name = trim($name);
}
unset($name);

print_r($names);

実行結果は次の通りです。

Array
(
    [0] => Tanaka
    [1] => Sato
    [2] => Suzuki
)

各要素を直接加工する処理では、参照付きforeachを使うとシンプルに書けます。

配列内の連想配列を加工したい場合

配列の中に連想配列が入っている場合も、参照付きforeachは使いやすいです。

$posts = [
    ['title' => '記事A', 'views' => 120],
    ['title' => '記事B', 'views' => 80],
];

foreach ($posts as &$post) {
    $post['isPopular'] = $post['views'] >= 100;
}
unset($post);

print_r($posts);

実行結果は次のようになります。

Array
(
    [0] => Array
        (
            [title] => 記事A
            [views] => 120
            [isPopular] => 1
        )

    [1] => Array
        (
            [title] => 記事B
            [views] => 80
            [isPopular] => 
        )
)

このように、配列内の各要素にフラグや計算結果を追加したい場合には、参照付きforeachが役立ちます。

参照付きforeachを避けたほうがよいケース

表示や集計だけなら参照は不要

配列の中身を表示するだけなら、参照付きforeachは不要です。

$items = ['A', 'B', 'C'];

foreach ($items as $item) {
    echo $item . PHP_EOL;
}

このような処理で、

foreach ($items as &$item) {
    echo $item . PHP_EOL;
}
unset($item);

と書く必要はありません。

参照を使うと、unset()忘れによるバグのリスクが増えます。

元の配列を変更しないのであれば、通常のforeachを使うべきです。

合計値を計算するだけなら参照は不要

合計値を計算する処理でも、参照付きforeachは不要です。

$items = [
    ['name' => 'Apple', 'price' => 100],
    ['name' => 'Banana', 'price' => 200],
    ['name' => 'Orange', 'price' => 300],
];

$total = 0;

foreach ($items as $item) {
    $total += $item['price'];
}

echo $total;

この場合、元の$itemsを書き換える必要はありません。

したがって、参照付きforeachを使うメリットはほとんどありません。

新しい配列を作るだけなら参照は不要

元の配列を変更せず、新しい配列を作る場合も、参照付きforeachは不要です。

$prices = [100, 200, 300];

$pricesWithTax = [];

foreach ($prices as $price) {
    $pricesWithTax[] = (int) floor($price * 1.1);
}

print_r($pricesWithTax);

この場合、元の$pricesはそのまま残ります。

新しい配列を作る処理では、通常のforeachのほうが安全で分かりやすいです。

参照付きforeachの代替方法

キーを使って元の配列を更新する

元の配列を変更したい場合でも、必ず参照付きforeachを使う必要はありません。

キーを使って元の配列を更新する方法もあります。

$prices = [100, 200, 300];

foreach ($prices as $key => $price) {
    $prices[$key] = (int) floor($price * 1.1);
}

print_r($prices);

実行結果は次の通りです。

Array
(
    [0] => 110
    [1] => 220
    [2] => 330
)

この方法なら、ループ変数が最後の要素への参照として残る問題は起きません。

参照付きforeachに慣れていない場合や、unset()忘れを避けたい場合は、キーを使って更新する方法も有効です。

連想配列でもキーを使って更新できる

多次元配列でも、キーを使って更新できます。

$users = [
    ['name' => 'Tanaka', 'score' => 80],
    ['name' => 'Sato', 'score' => 95],
];

foreach ($users as $key => $user) {
    $users[$key]['passed'] = $user['score'] >= 90;
}

print_r($users);

このように、$users[$key]に対して代入すれば、元の配列を変更できます。

参照付きforeachと比べると少し記述は長くなりますが、参照が残る問題を避けられる点がメリットです。

array_mapで新しい配列を作る

配列を変換して新しい配列を作るなら、array_map()も選択肢になります。

$prices = [100, 200, 300];

$pricesWithTax = array_map(function ($price) {
    return (int) floor($price * 1.1);
}, $prices);

print_r($pricesWithTax);

実行結果は次のようになります。

Array
(
    [0] => 110
    [1] => 220
    [2] => 330
)

元の配列を置き換えたい場合は、次のようにも書けます。

$prices = [100, 200, 300];

$prices = array_map(function ($price) {
    return (int) floor($price * 1.1);
}, $prices);

print_r($prices);

array_map()は、各要素を変換して新しい配列を返す処理に向いています。

ただし、複雑な連想配列を何段階も加工する場合は、通常のforeachのほうが読みやすいこともあります。

参照付きforeach中に配列構造を変更する場合の注意点

ループ中の追加・削除は避けたほうがよい

参照付きforeachの中で、対象の配列に要素を追加したり削除したりする処理は、できるだけ避けたほうが安全です。

例えば、次のようなコードです。

$array = [1, 2, 3];

foreach ($array as &$value) {
    if ($value === 2) {
        $array[] = 99;
    }
}
unset($value);

このようなコードは、処理の流れが分かりにくくなります。

また、foreachは値で回す場合と参照で回す場合で、配列の変更に対する挙動が異なることがあります。

そのため、ループ中に配列構造を変更すると、読み手が挙動を予測しづらくなります。

値の変更と構造変更は分ける

安全に書くなら、値の変更と配列の追加・削除は分けるのがおすすめです。

例えば、追加したい要素を別の配列にためておき、ループ後にまとめて追加します。

$array = [1, 2, 3];
$itemsToAdd = [];

foreach ($array as $value) {
    if ($value === 2) {
        $itemsToAdd[] = 99;
    }
}

$array = array_merge($array, $itemsToAdd);

print_r($array);

このように処理を分けると、コードの意図が明確になります。

参照付きforeachを使う場合は、基本的に「各要素の値を変更するだけ」に限定したほうが安全です。

関数内で参照付きforeachを使う場合

関数内でも参照は同じスコープ内に残る

関数内で参照付きforeachを使う場合も、unset()は意識しておくべきです。

function updatePrices(array $items): array
{
    foreach ($items as &$item) {
        $item['price'] *= 1.1;
    }

    foreach ($items as $item) {
        // ここで意図しない書き換えが起こる可能性がある
    }

    return $items;
}

このコードでは、最初のforeachが終わった後も、$itemは最後の要素への参照を持っています。

その状態で、後続のforeachでも同じ$itemを使っているため、最後の要素が書き換わる可能性があります。

安全にするなら、次のように書きます。

function updatePrices(array $items): array
{
    foreach ($items as &$item) {
        $item['price'] *= 1.1;
    }
    unset($item);

    foreach ($items as $item) {
        // 安全
    }

    return $items;
}

関数内であっても、同じ関数スコープの後続処理では参照が影響します。

関数がすぐ終了する場合でもunsetしておくと安全

次のように、参照付きforeachの直後に関数が終了する場合、実害が出ないこともあります。

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

    return $array;
}

この場合、関数が終了すればローカル変数も破棄されるため、後続処理で$valueを使って配列が壊れる問題は起こりにくいです。

ただし、後から処理が追加される可能性を考えると、次のように書いておくほうが安全です。

function doubleValues(array $array): array
{
    foreach ($array as &$value) {
        $value *= 2;
    }
    unset($value);

    return $array;
}

参照付きforeachを書いたら、その直後にunset()を書くというルールにしておくと、保守時の事故を防ぎやすくなります。

参照付きforeachを書くときのチェックポイント

元の配列を変更する必要があるか確認する

参照付きforeachは、元の配列を直接変更したい場合に使います。

表示、集計、検索、判定だけであれば、通常のforeachで十分です。

foreach ($items as $item) {
    // 表示・集計・判定など
}

元の配列を変更しない処理で参照を使うと、不要なリスクが増えます。

ループ直後にunsetしているか確認する

参照付きforeachを使ったら、直後にunset()を書きます。

foreach ($items as &$item) {
    // 元の配列を変更する処理
}
unset($item);

このunset()は、最後の要素への参照を残さないための重要な処理です。

後続処理で同じ変数名を使っていないか確認する

unset()していない状態で、同じ変数名を後続のforeachに使うと危険です。

foreach ($items as &$item) {
    // 加工
}

// unset($item); がない

foreach ($items as $item) {
    // 危険
}

安全にするなら、次のようにします。

foreach ($items as &$item) {
    // 加工
}
unset($item);

foreach ($items as $currentItem) {
    // 安全
}

unset()を入れたうえで、変数名も分けるとより安心です。

ループ中に配列の追加・削除をしていないか確認する

参照付きforeachの中で、対象配列の要素を追加・削除すると、処理が分かりにくくなります。

foreach ($array as &$value) {
    if ($value === 2) {
        $array[] = 99;
    }
}
unset($value);

このような処理は、可能であれば分けて書くほうが安全です。

実務でのおすすめルール

基本ルール

参照付きforeachを使う場合は、次の形を基本にすると安全です。

foreach ($array as &$value) {
    // 元の配列要素を直接変更する
}
unset($value);

この書き方を徹底するだけで、最後の要素が意図せず書き換わるバグをかなり防げます。

より安全な書き方

後続処理がある場合は、変数名も分けるとより安全です。

foreach ($array as &$value) {
    $value = trim($value);
}
unset($value);

foreach ($array as $currentValue) {
    echo $currentValue . PHP_EOL;
}

unset()によって参照を切り、さらに別の変数名を使うことで、コードの意図が明確になります。

参照を使わない選択肢も検討する

単純な更新であれば、キーを使った代入も検討できます。

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

この方法なら、参照付きforeach特有の「最後の要素への参照が残る」問題は起きません。

参照付きforeachは便利ですが、必ず使うべきものではありません。

読みやすさ、安全性、処理内容に応じて使い分けるのが大切です。

まとめ

参照付きforeachではunsetが重要

PHPのforeachでは、値変数の前に&を付けることで、配列要素を参照として扱えます。

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

この書き方をすると、ループ内で$valueを変更したときに、元の配列要素も直接変更されます。

ただし、参照付きforeachの後は、ループ変数が最後の要素への参照を持ち続けます。

そのため、直後にunset($value)を書いて、ループ変数を破棄するのが安全です。

参照付きforeachは必要なときだけ使う

参照付きforeachは、元の配列を直接変更したい場合に便利です。

一方で、表示、集計、新しい配列の作成だけなら、通常のforeacharray_map()で十分です。

実務では、次のように判断するとよいです。

やりたいことおすすめの書き方
配列を表示する通常のforeach
合計を計算する通常のforeach
新しい配列を作る通常のforeachまたはarray_map()
元の配列を直接変更する参照付きforeachまたはキー代入
参照によるバグを避けたいキー代入

特に初心者のうちは、参照付きforeachを使うたびに、次の3点を確認すると安全です。

チェック項目内容
元の配列を直接変更する必要があるか不要なら参照は使わない
ループ直後にunset()しているか参照を残さないために重要
後続処理で同じ変数名を使っていないかunset()なしだと危険

PHPの参照付きforeachは便利ですが、扱いを間違えると分かりにくいバグを生みます。

基本は、参照付きforeachを使ったら直後にunset()することです。

foreach ($array as &$value) {
    // 元の配列要素を変更する
}
unset($value);

このルールを守るだけで、参照付きforeachによる多くのトラブルを防げます。

以上、PHPのforeachで参照渡しをしたい時の注意点についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次