PHP5とPHP4の変数の参照渡しの違い | A Day In The Boy's Life

A Day In The Boy's Life

とあるエンジニアのとある1日のつぶやき。

テーマ:

PHP5における変数の参照渡しについて改めて調べてみると、結構間違った記述をしているサイトが見受けられます。
(PHP5に関するリリース情報のアナウンスの仕方が悪いという意見が多いですけど)


PHP5とPHP4における変数の参照渡しによる挙動の違い


変数の参照渡しとコピーの違いは、下記のスクリプトを実行してみれば明らかです。


<?php

$a = "hoge";

// ここの受け渡しがポイント
$b = $a;

$a = "foo";

echo "\$a is " . $a . "\n";
echo "\$b is " . $b . "\n";

?>

$ php non-reference.php
$a is foo
$b is hoge

<?php

$a = "hoge";

// ここの受け渡しがポイント
$b = & $a;

$a = "foo";

echo "\$a is " . $a . "\n";
echo "\$b is " . $b . "\n";

?>

$ php reference.php
$a is foo
$b is foo


要は、参照渡しは変数のアドレスが渡されるので、元の変数($a)が書き換わると、参照渡しされた変数($b)の値も変わってしまいます。


この辺りの挙動の違いを、昔は良くわからずにいて苦労したのですが、イメージとしてはWindowsにおけるショートカット、Linuxにおけるエイリアスのようなものでしょうか。
例えばショートカット先のWordファイルの中身が書き換わったら、ショートカットも書き換わったWordファイルをきちんと参照してくれます。

同じ内容のWordファイルをコピーしてしまったら、一方を書き換えたら他方は書き換わっていません。

参照を使うの際は、そういう動きをさせたいかどうかさせたいかどうかも選択肢の一つになってきます。

他にも、データをコピーしてしまうと2倍のメモリ空間が使われること意なるので容量の無駄になったり、データをコピーするというのは、コストがかかる動きなので多用するとパフォーマンスに影響がでるといったこともあります。


上記のプログラムはいづれもPHP5で実行してみた結果です。
PHP4での参照渡しがデフォルトで組み込まれているのであれば、この2つのスクリプトは同じ結果となるはずなのにそうではありません。


PHP5における参照が変わったというのが何を意味するかについては、下記のサイトに詳しく書かれています。


はじめに-PHP変数管理解説(1)-参照と値渡しの明確な理解のために @ CPA-LABテクニカル


あと、PHPのマニュアルのオブジェクトと参照 の章も読んでおいたほうが良いでしょう。


PHP5で値のコピー($a = $bのような受け渡し)をした場合、最初はアドレス空間を共有して使っているけど、一方に別の値が代入されたときに別のアドレス空間に分離させる、という動きをするようになっています。
この仕組みをコピーオンライト(Copy On Write)といいます。


データをコピーするという動作は時間がかかる処理になるので、なるべくその動作を少なくするために、必要になるまでは同じ値なら同じデータを「参照」させるようにしよう、というのがPHP5の参照の正体です。
(PHP4の参照とは違う動きです)



オブジェクトの参照渡し


ただし、オブジェクトに関してはデフォルトで参照渡しになっています。


<?php

class hoge {

    var $var = NULL;

    function foo() {
        return $this->var;
    }
}

$obj1 = new hoge();
$obj2 = $obj1;

$obj1->var = "bar";

echo "\$obj1->var is " . $obj1->foo() . "\n";
echo "\$obj2->var is " . $obj2->foo() . "\n";

?>

上記のスクリプトを実行してみると


$ php reference-class.php
$obj1->var is bar
$obj2->var is bar

のようにお互いのオブジェクトのメンバ変数が書き換わっています。
var_dumpで内容を見てみても同じオブジェクトを参照していることがわかります。


object(hoge)#1 (1) {
  ["var"]=>
  string(3) "bar"
}
object(hoge)#1 (1) {
  ["var"]=>
  string(3) "bar"
}

このように、変数をコピー($obj2 = $obj1)しているのに、その後の$obj1のメンバ変数varの値を書き換えると$obj2のメンバ変数まで書き換わっていることから、オブジェクトは参照渡しされていることがわかります。
これを、PHP4で実行させると動作が異なります。


$ php -v
PHP 4.3.9 (cgi) (built: Jul 15 2008 10:14:59)
Copyright (c) 1997-2004 The PHP Group
Zend Engine v1.3.0, Copyright (c) 1998-2004 Zend Technologies

$ php reference-class.php
$obj1->var is bar
$obj2->var is

var_dumpで中身を見ると、$obj2のメンバ変数varは初期値のNULLのままです。


object(hoge)(1) {
  ["var"]=>
  string(3) "bar"
}
object(hoge)(1) {
  ["var"]=>
  NULL
}

PHP4のようにオブジェクトをコピーしたい場合は、cloneを付けてオブジェクトをコピーさせます。


<?php

class hoge {

    var $var = NULL;

    function foo() {
        return $this->var;
    }
}

$obj1 = new hoge();
// cloneを付けてオブジェクトをコピーする
$obj2 = clone $obj1;

$obj1->var = "bar";

echo "\$obj1->var is " . $obj1->foo() . "\n";
echo "\$obj2->var is " . $obj2->foo() . "\n";

?>

$ php copy-class.php
$obj1->var is bar
$obj2->var is


規模が大きくなってくると、このような参照渡しを使ってパフォーマンスを改善することができるメリットもある一方で、挙動をしっかりと把握しておかないと、思わぬバグを作ることになってしまいます。
特にPHPの場合、バージョンによって挙動が違うものが多いので、気をつけておきたい書き方の1つになります。