LoginSignup
1
1

More than 1 year has passed since last update.

PHPのforeachでは、多くの場合、参照渡しの方がいい?

Last updated at Posted at 2022-06-10
../

PHPでは、関数を呼び出すとき、引数を「値渡し」(&なし)にするか、「参照渡し」(&あり)にするか決めないといけない。配列(array)、文字列(string)、他のクラスからインスタンス化したオブジェクトを「構造体」と呼ぶことにすると、以下のルールで決めることになる。

  • 関数内で引数で渡された構造体に副作用を与えたいとき --> 参照渡し
  • 関数内で引数で渡された構造体に副作用を与えたくないとき --> 値渡し

値渡しでは、引数で与えられた構造体をコピーして受け取る。関数内で引数で与えられた構造体に変更を施しても呼び出し元の構造体が書き換わることはない。一方、参照渡しでは、値をコピーせずに構造体への参照(ポインタというか、アドレスというか)のみを渡す。関数内で引数で与えられた構造体に変更を施すことは、呼び出し元の構造体が書き換わることになる。

関数呼び出しでの値渡しと参照渡しの確認

配列(array)、文字列(string)の例で確認しておこう。参照渡しの場合、呼び出し元の構造体に影響を与えることが確認できる。

<?php
function func1(array $arr){
  $arr[] = 30;
  echo 'func1::$arr = ';
  var_dump($arr);
}

function func2(array &$arr){
  $arr[] = 30;
  echo 'func2::$arr = ';
  var_dump($arr);
}

echo '整数配列の値渡し'.PHP_EOL;
$array1 = array(10,20);
func1($array1);
echo '$array1 = ';
var_dump($array1);

echo '整数配列の参照渡し'.PHP_EOL;
$array2 = array(10,20);
func2($array2);
echo '$array2 = ';
var_dump($array2);

function str1(string $str): string {
  $str[1] = '@';
  echo 'str1::$str = ' . $str . PHP_EOL;
  return $str;
}

function str2(string &$str): string {
  $str[1] = '@';
  echo 'str2::$str = ' . $str . PHP_EOL;
  return $str;
}

echo '文字列の値渡し'.PHP_EOL;
$string1 = 'abc';
$result1 = str1($string1);
echo '$string1 = ' . $string1 . PHP_EOL;
echo '$result1 = ' . $result1 . PHP_EOL;

echo '文字列の参照渡し'.PHP_EOL;
$string2 = 'abc';
$result2 = str2($string2);
echo '$string2 = ' . $string2 . PHP_EOL;
echo '$result2 = ' . $result2 . PHP_EOL;
?>

foreachでの値渡しと参照渡しの確認

ここからが本題であるが、配列に対するforeachの場合、asで受け取る配列要素は、値渡しで受け取るべきか、参照渡しで受け取るべきか。ほとんどの場合、配列要素に副作用を与えることはないので、コピーのコストが大きい値渡しにせずに、コストの小さい参照渡しにした方がいいだろう。

<?php
echo '配列に対するforeachでの値渡し'.PHP_EOL;
$array1 = array('abc', '123');
foreach ($array1 as $str){
  $str[1] = '@';
  echo $str.PHP_EOL;
}
var_dump($array1);

echo '配列に対するforeachでの参照渡し'.PHP_EOL;
$array2 = array('abc', '123');
foreach ($array2 as &$str){
  $str[1] = '@';
  echo $str.PHP_EOL;
}
unset($str); // foreachで参照渡しを使ったときは安全のため
var_dump($array2);
?>

上記のように、配列要素に副作用を与える場合のみ、値渡しにすればよい。多くの場合、参照渡しで十分である。$str[1] = '@';のように要素に変更を加えるような場合、値渡しすべきか、参照渡しにすべきか考えればよい。

PHP5から7になるとき内部構造が改善(コメント参照)

コメント欄にアドバイスをいただきました。「PHP5から7になるときに、一般的なケースでは &を使わない方がパフォーマンス上有利になるように内部構造が改善されている」そうです。intなどのプリミティブな型や文字数の少ないstringなどでは、参照渡しにするメリットはないかもしれません。

foreachでは、配列要素がユーザー定義の構造体の場合は参照渡しになる!

foreachでは、配列要素がユーザー定義の構造体、つまりクラス定義からインスタンス化したオブジェクトが配列要素である場合、値渡しで受け取っても参照渡しになっている。以下のようなUserというクラスを配列要素にして確認してみる。

<?php
namespace samples;
class User {

  private $id;
  private $name;
  
  public function __construct(string $id, string $name) {
    $this->setId($id);
    $this->setName($name);
  }
  public function getId(): string { return $this->id; }
  public function getName(): string { return $this->name; }
  public function setId(string $id): void { $this->id = $id; }
  public function setName(string $name): void { $this->name = $name; }
}

以下のような2つのテストコードで値渡しと参照渡しを確認してみる。配列にはインスタンスを2つ入れておく。

$users = array();
$users[] = new User('U001', 'Aさん');
$users[] = new User('U002', 'Bさん');
foreach ($users as $user){			// 値渡し
  if ($user->getId() == 'U002'){
  	$user->setName('山本さん');
  }
}
var_dump($users);

$users = array();
$users[] = new User('U001', 'Aさん');
$users[] = new User('U002', 'Bさん');
foreach ($users as &$user){			// 参照渡し
  if ($user->getId() == 'U002'){
  	$user->setName('山本さん');
  }
}
unset($user); // foreachで参照渡しを使ったときは安全のため
var_dump($users);

結果はどちらの場合でも、以下のようになる。値渡しでも参照渡しでも「山本さん」に置き換わっている。つまり、foreachの場合、要素がユーザー定義の構造体のとき、値渡しで書いても、オブジェクトはコピーされずに参照渡しになっている。オブジェクトの所在を示すアドレスが渡されている。この挙動は、配列要素が整数などのプリミティブな型、文字列、配列の場合とは異なる。配列要素がプリミティブな型、文字列、配列の場合に値渡しにすると、コピーされる。

array(2) {
  [0] =>
  class samples\User#1 (2) {
    private $id =>
    string(4) "U001"
    private $name =>
    string(7) "Aさん"
  }
  [1] =>
  class samples\User#2 (2) {
    private $id =>
    string(4) "U002"
    private $name =>
    string(12) "山本さん"
  }
}

配列要素にどのように影響するかを再度、見ておく。配列要素は何でもよいが、以下では整数としておく。

$numbers = array();
$numbers[] = 10;
$numbers[] = 20;
foreach ($numbers as $number){			// 値渡し
  if ($number == 20){
    $number = 999;
  }
}
var_dump($numbers);

値渡しだと、元の配列には影響しない。値そのものがコピーされている。

array(2) {
  [0] =>
  int(10)
  [1] =>
  int(20)
}

参照渡しだと、元の配列に影響する。2番目の要素が20から999に書き換わっている。値が置かれているメモリ上の領域を示すアドレスが渡されている。

$numbers = array();
$numbers[] = 10;
$numbers[] = 20;
foreach ($numbers as &$number){			// 参照渡し
  if ($number == 20){
    $number = 999;
  }
}
unset($number); // foreachで参照渡しを使ったときは安全のため
var_dump($numbers);
array(2) {
  [0] =>
  int(10)
  [1] =>
  int(999)
}

また、配列要素が文字列の場合に参照渡しすると、その文字列を変更する操作が元の配列の要素に影響する。前述の$str[1] = '@';の例で分かるだろう。例示は割愛。文字列自体はコピーされず、その文字列の所在を示すアドレスが渡されている。

最後に、配列要素が配列の場合でも確認しておこう。

$arrays = array();
$arrays[] = [10, 20];
$arrays[] = [30, 40];
foreach ($arrays as $array){			// 値渡し
  if ($array[0] == 30){
    $array[1] = 999;
  }
}
var_dump($arrays);

値渡しだと、元の配列には影響しない。コピーされている。

array(2) {
  [0] =>
  array(2) {
    [0] =>
    int(10)
    [1] =>
    int(20)
  }
  [1] =>
  array(2) {
    [0] =>
    int(30)
    [1] =>
    int(40)
  }
}

参照渡しだと、元の配列に影響を与える。40が999に書き換わっている。コピーされていない。配列の所在を示すアドレスが渡されている。

$arrays = array();
$arrays[] = [10, 20];
$arrays[] = [30, 40];
foreach ($arrays as &$array){			// 参照渡し
  if ($array[0] == 30){
    $array[1] = 999;
  }
}
unset($array); // foreachで参照渡しを使ったときは安全のため
var_dump($arrays);
array(2) {
  [0] =>
  array(2) {
    [0] =>
    int(10)
    [1] =>
    int(20)
  }
  [1] =>
  array(2) {
    [0] =>
    int(30)
    [1] =>
    int(999)
  }
}
../
1
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1