PHP5 のオブジェクトに関するよくある間違いとメモリ管理 (コピーオンライト) とオブジェクトの取り扱い

PHP5 でのよくある間違い

もしあなたが OOP アプリケーションを PHP4 から PHP5 へマイグレートした経験をお持ちなら、きっとこんな話を聞いたことがあると思います。

「PHP5 ではデフォルトで、オブジェクトは参照によるコピーをするんだよ」

誰がそう言おうと、それは誤りです。

公平に言えば、それは悪意のある嘘ではありません。なぜなら、オブジェクトは参照 (reference) のような振る舞いを確かにするからです。でも、それは参照ではないのです。

次の例を見てください。

<?php

$a = new stdClass;
$b = $a;

$a->foo = 'bar';
var_dump($a);

/* この時点では、$a と $b はまさに同じオブジェクトインスタンスを共有
  * していることに注意してください。これは参照のような振る舞いと言って
  * よいでしょう。
  */

$a = 'baz';

var_dump($a);
var_dump($b);

/* この時点で $b は依然として元のオブジェクトです。
  * $a は参照を持っていたものの、ただの文字列に
  * 変わってしまっています。
  */

?>

上記の実行結果は以下のようになります。

$a -----
object(stdClass)#1 (1) {
  ["foo"]=>
  string(3) "bar"
}
$a -----
string(3) "baz"
$b -----
object(stdClass)#1 (1) {
  ["foo"]=>
  string(3) "bar"
}

上では何が起きていたのでしょうか?

一番簡単に答えるために、オブジェクトの内部データ構造を説明します。

PHP オブジェクト、変数のデータ構造

PHP5 ではオブジェクトを含む変数では、そのインスタンスを識別するために単純な数値を使います。 オブジェクトに対して何かを行うときは、実際のインスタンスを取り出すために、ルックアップテーブルとその数値を使います。 一方、PHP4 では配列を含む変数はそのオブジェクトを、実際のプロパティーテーブルを持つことで識別します。

つまりこれの意味するところは、PHP5 オブジェクトに新しい変数を (明示的な参照によってではなく) 割り当てる時、 その整数のハンドルが新しい変数にコピーされるということです。同じ番号 (整数値) ですから、 それらは共に同じインスタンスを指します。

しかしながら、PHP4 のオブジェクトを割り当てるということは、全てのプロパティをコピーし、 新しいインスタンスを生成します。他方への変更が他方へ影響しないようにするためです。

違う言い方をすれば、PHP4 オブジェクトは基本的に「関数が関連付けされた配列」です。 PHP5 のオブジェクトは「関数が疎に関連付けされたリソース」です。

変数のコピーは「コピー」ではない?!

変数の「コピー」は厳密にコピーという意味ではありません。次の例をみてください。

<?php
$a = 'foo';
$b = $a;
$a = 'bar';
?>

PHP のことを知っていれば、このコードブロックの最後で $a は 'bar' となり、 $b は 'foo' のままであることはお分りになるでしょう。

知らないことがあるとすれば、$a の中の 'foo' は実際にはコピーされてはいなかった、ということです。

PHP がしていることを理解するためには、変数の内部データ構造を理解し、それとユーザーがコード上で目にする 変数名とどのように関連付けされるかを理解しなければなりません。

変数の実際の中身は zval と呼ばれているのですが、これは4つの部分から構成されています。

ひとつは、type (型) (例えば NULL とか Integer だとか String だとか、です) 二つ目は value (値)、 三つ目は is_ref フラグ (これはこの値が参照かそうでないかを示すフラグ)、四つ目は refcount (参照カウント) で、これはこの値が何度共有されているか示します。

あなたが変数と思っているもの (例えば $x) は、実際にはただのラベルであり、 そのラベルは実際の値を含む zval を見つけるための参照値として使われます。 これは連想配列におけるキーに似ていますが、実際に仕組みは同一です。

あなたが最初に変数を作成したときに、PHP はそれに対して新しい zval を割り当て、 値を格納し、その値とラベルと関連付けされます。

例えば $x = 123; としたときは、次のようになります。

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

ここで、refcount は 1 です。なぜならこの zval はひとつのラベル 'x' によってのみ参照されているからです。

次に $y =& $x; として $y に参照を格納すると、同じ zval が再利用されます。

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

この場合は単純に 'y' というラベルがもとの zval と関連付けされた状態になります。

この状態であると、$x の値を変えると、$y の値も変わったようにみえます。なぜなら、 それらは両方とも同じ内部データを参照しているからです。

しかし、ここで参照割当てではない方法で割り当てていたら、どうなっていたでしょうか。

つまり、上の例で $y =& $x; ではなく、$y = $x として割り当てた場合です。

この場合は次のようになります。

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

この場合も $x に関連付けされた zval は再利用されます。唯一の違いは、is_ref が 0 (false) であるということです。

これはコピーオンライト参照割当て (copy-on-write reference set)として知られています。 (これと対照的なものは上記で説明した 「全参照設定割当て (full-reference set)」です)

このフラグが 0 であることによって、PHP エンジンに対して、もしこの値を誰かが変えようとしたら、 これへの他の全ての参照を全て切り離さなければならないということを知らせます。

具体的に言えば $x = 456 とすると、以下のようになります。

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

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説