最新のPHPニュース

PHPでの内部変数の扱われ方 - PHP5のオブジェクトコピーでありがちな勘違い

2007年01月29日

オブジェクト指向で作られたアプリケーションをPHP4からPHP5への移行した経験のある人によく見受けられるのですが、PHP5ではデフォルトでオブジェクトのコピーは参照のコピーとなる、というのは間違いです。確かにオブジェクトは「参照のように」扱うことができますが、正確にはオブジェクトへの参照ではありません。よく似た動きをするので大変間違いやすいのです。PHP開発者であるSara GolemonがPHPの内部変数について興味深い記事を書いています。まずは以下のコードを見てください。

   1  <?php
  2  $a 
= new stdClass;
  3  
$b $a;
  4  
$a->foo 'bar';
  5  
var_dump($b);
  6  
// この時点では $a と $b は同じインスタンス
  7  // ここまでは参照渡しと同じ動きをする
  8  
$a 'baz';
  9  
var_dump($b);
 10  
// $b は変更が適用される前の $a のまま
 11  // 参照渡しであれば $b も変更されるはずだが、変更されない
 12  
?>

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

object(stdClass)#1 (1) {
  ["foo"]=>
  string(3) "bar"
}
object(stdClass)#1 (1) {
  ["foo"]=>
  string(3) "bar"
}

参照渡しで$aと$bが同じものだと思っている方は不思議に思うかもしれません。PHP内部でどのようにオブジェクト変数が扱われているかというと、まずPHP5では、オブジェクトが格納されている変数に内部的に保存されているのは、インスタンスを参照するための整数値となります。オブジェクトに何らかのアクションが実行されるときに、その整数値を利用して実際のインスタンスを参照するわけです。それに対して、PHP4では実際のプロパティのテーブルそのものが格納されます。 

そのため、PHP5でオブジェクト変数を別の変数に代入すると、同じインスタンスを示す番号がコピーされるので一見参照渡しのように見えてしまうのです。それに対してPHP4では、新しいインスタンスが作成され、すべてのプロパティがコピーされます。別の言い方をすれば、PHP4でのオブジェクトは「関数が関連づけられた配列」で、PHP5ではファイルポインタのような「リソース」で関数が関連付いていると考えてもよいかもしれません。

次に、以下のコードを見てください。

   1  <?php
  2  $fp 
fopen('foo.txt''w');
  3  
$otherVar $fp;
  4  
fwrite($fp"One\n");
  5  
fwrite($otherVar"Two\n");
  6  
fclose($fp);
  7  
fwrite($otherVar"Three\n");
  8    
/* ファイルはすでに閉じているのでエラーとなる */
  9  
?>

このコードを実行した場合、PHP4でもPHP5でも最後のfwrite関数での書き込みはエラーとなってしまいます。PHP4で考えるのであれば、これと同じようなことがPHP5のオブジェクト変数のコピーでも起きると考えればよいでしょう。

さらに詳しく

先ほどはオブジェクト変数のコピーについて見てみましたが、より一般的な変数のコピーについて見てみましょう。

   1  <?php
  2  $a 
'foo';
  3  
$b $a;
  4  
$a 'bar';
  5  
?>

このコードを実行すると、$aは「bar」で$bで「foo」であることは自明でしょう。しかし、このコードの実行中に一度も2つの「foo」という文字列データが存在することはない、ということを知らない人は多いのではないでしょうか。

PHPがどのように動作するのかを理解するためには、変数の内部構造と、変数名との関連性を理解する必要があります。変数はPHP内部では以下の4つの値を持っており、zvalと呼ばれています。

  • type(変数の型)
  • value(実際の値)
  • is_ref(変数が参照かどうかを示すフラグ)
  • refcount(この値を共有しているラベルの数)

変数名は値が格納されているzvalを探すために使用されるだけで、zvalに付けられたラベルのようなものです。変数名がキーとなっている連想配列だと考えておいてください。

実際に $x = 123; が実行されると、PHPはzvalの領域を確保し、そこに値を格納します。さらに、zvalと「x」というラベルを関連づけます。以下のようなイメージです。「x」というラベルにzvalが関連づけられているという意味です。

$x = 123;
'x' => 
 zval( type => IS_LONG, value.lval = 123, is_ref = 0, refcount = 1)

zvalは「x」というラベルにのみ関連づけられているので、refcountは1となります。次に$y =& $x;という参照渡しが実行された場合です。

$x = 123;
$y =& $x;
'x', 'y' => 
 zval ( type => IS_LONG, value.lval = 123, is_ref = 1, refcount = 2 )

「x」と「y」2つのラベルに対して同じzvalが関連づけられます。このis_refが1となることで、参照渡しであることがわかり、refcountが2であるため2つのラベルが関連づけられていることもわかります。参照渡しではなく直接$y = $x;と代入された場合ですが、以下のようになります。

$x = 123;
$y = $x;
'x', 'y' => 
 zval ( type => IS_LONG, value.lval = 123, is_ref = 0, refcount = 2 )

is_refが0となっていることのみ異なりますが、ほとんど同じとなります。この操作はコピーオンライトとして知られています。フラグが0となっているのでZendエンジンはどちらかの変数への書き込みを検知し、その時点で実際にあたいをコピーします。そのため、$x = 456;とすると以下のようになります。

$x = 123;
$y = $x;
$x = 456;
'y' =>
 zval ( type => IS_LONG, value.lval = 123, is_ref = 0, refcount = 1 )
'x' =>
 zval ( type => IS_LONG, value.lval = 456, is_ref = 0, refcount = 1 )

元々$xに割り当てられていたzvalは$yが引き継ぐことになり、$xには新しいzvalが割り当てられます。また、$yのzvalのrefcountは1となります。

参照を多用することの危険性

まず、以下のコードを見てください。

   1  <?php
  2  $a 
'foo';
  3  
$b $a;
  4  
$c =& $a;
  5  
?>

1行ずつzvalがどうなるか見ていきましょう。まず最初の操作で$aにzvalが割り当てられます。(value.str.lenについては簡単のため省略します。)

'a' =>
 zval ( type => IS_STRING, value.str.val = 'foo', is_ref = 0, refcount = 1 )

次の操作で$aに割り当てられていたzvalが新たに$bに関連づけられます。

'a', 'b' =>
 zval ( type => IS_STRING, value.str.val = 'foo', is_ref = 0, refcount = 2 )

最後の操作で問題がでてしまいます。すでにzvalはコピーオンライトの状態となっているので、is_refを1とすることはできません。そのため、zvalをもう一つ作成し、この問題を解決します。

'b' =>
 zval ( type => IS_STRING, value.str.val = 'foo', is_ref = 0, refcount = 1 )
'a', 'c' =>
 zval ( type => IS_STRING, value.str.val = 'foo', is_ref = 1, refcount = 2 )

このように参照渡しをすることでzvalの領域が確保され、無駄なメモリが消費されてしまいます。こんな操作はしていないと思うかもしれませんが、コピーオンライトは関数に渡される引数など、色々な場所で使用されているので、参照渡しをしている部分でこの無駄なコピーが行われているは意外と多いのです。

関連リンク

この記事へのトラックバックURL

GRANADA Hatena @ sotarok

2008年06月05日 02:17 [PHP]オブジェクトの参照渡しと値渡しについて

唐突ですが、今日は昨日 id:kensuu に聞かれてちゃ...

続きを読む