言語が違えばルールや常識も変わるものです。
この記事ではPHP8.0技術者認定試験上級資格取得を目指すにあたり、PHPを理解するために知っておくべきことについてさらっとまとめてみました。
ぜひご参考までに読んでいただければと思います。
目次
1. シャローコピーとディープコピー
- シャローコピーについて
- ディープコピーについて
- __cloneメソッドについて
2. コピーオンライト
3. リファレンスカウントの原理
- zvalコンテナ
4. まとめ
シャローコピーとディープコピー
PHPにおいて、変数の代入は値渡し(ディープコピー)が規定ですが、オブジェクト変数だけは例外的に参照渡し(リファレンス渡し/シャローコピー)が規定となっています。$p1 = new Person('太郎', 'PHP');
$p2 = $p1; // オブジェクトを代入
そのため上記のような場合、オブジェクトのコピー$p2で名前を変更する処理を加えると、その変更処理はコピー元である$p1にも反映されてしまいます。
しかし、コピー先では別の名前を設定できるようにしたい(ディープコピーしたい)場合があります。この時登場するのが「__cloneメソッド」です。
ここからは順を追って説明していきますので、まずは、シャローコピーとディープコピーについて見ていきます。
シャローコピーについて
見ている元データは同じですが、データ自体をコピーしているわけではありません。
そのため、同じデータを指す新しい変数を作成しているだけのコピーとなります。
引用元:ただ屋ぁのブログ
ディープコピーについて
シャローコピーとは異なり、新しい変数を作り、元データの実体そのものもコピーします。
いわゆる、一般的にイメージされるコピーはディープコピーのことを指します。
引用元:ただ屋ぁのブログ
__cloneメソッドについて
clone命令はオブジェクトのコピーを生成するための命令ですが、clone命令の規定の挙動はシャローコピーです。
上述しているように、コピー先では別の名前を設定できるようにしたい(ディープコピーしたい)場合は「__cloneメソッド」を使用します。
この「__cloneメソッド」は、clone命令による複製が完了したタイミングで呼び出されるメソッドで、コピー先のプロパティ値を強制的に変更することができます。
「__cloneメソッド」についてPHP公式リファレンスの例を用いて、解説していきます。
<?php
class SubObject
{
static $instances = 0;
public $instance;
public function __construct() {
$this->instance = ++self::$instances;
}
public function __clone() {
$this->instance = ++self::$instances;
}
}
class MyCloneable
{
public $object1;
public $object2;
function __clone()
{
// this->object のコピーを作成します。こうしないと、
// 同じオブジェクトを指すことになってしまいます。
$this->object1 = clone $this->object1;
}
}
$obj = new MyCloneable();
$obj->object1 = new SubObject();
$obj->object2 = new SubObject();
$obj2 = clone $obj;
print("元のオブジェクト\n");
print_r($obj);
print("クローンオブジェクト\n");
print_r($obj2);
?>
__constructメソッドは、オブジェクトが作成されるたびに$instancesをインクリメントして、$instanceにその値を設定します。
__cloneメソッドは、オブジェクトがクローンされるたびに$instancesをインクリメントして、$instanceにその値を設定します。
コードの処理としては以下のようになります。
①
$obj = new MyCloneable();
によって、MyCloneableクラスのインスタンスを新規作成②
$obj->object1 = new SubObject();
によって、$object1にSubObjectクラスのインスタンスが生成・代入され、SubObjectクラスの__construct()メソッドによって$instancesがインクリメントされる($instances:1)③
$obj->object2 = new SubObject();
によって、$object2にSubObjectクラスのインスタンスが生成・代入され、SubObjectクラスの__construct()メソッドによって$instancesがインクリメントされる($instances:2)④
$obj2 = clone $obj;
によって、$obj2に$objの複製(シャローコピー)が完了した段階で、MyCloneableクラスの__clone()が呼び出される⑤④の__clone()メソッドによって、$object1(SubObjectのインスタンス)が$object1に複製(シャローコピー)された段階で、SubObjectクラスの__clone()メソッドが呼び出されて$instancesがインクリメントされ($instances:3)、クローンされたオブジェクト($obj2)の$object1に代入される。
⑥
print_r($obj);
によって、元のオブジェクトが出力される
⑦print_r($obj2);
クローンオブジェクトが出力される
上記の説明の通り、__clone()メソッドを用いて強制的に必要な変更を行うことでディープコピーを実現している訳です。
コピーオンライト
PHPではコピーオンライトという仕組みが用いられています。コンピュータ内部で、ある程度大きなデータを複製する必要が生じたとき、愚直な設計では、直ちに新たな空き領域を探して割り当て、コピーを実行してしまいます。
ところが、もし複製したデータに対する書き換えがなければその複製は無駄だったことになるでしょう。
そこで、複製を要求されても、コピーをした振りをして、とりあえず原本をそのまま参照させる方が無駄はありません。ただし、そのままで本当に書き換えては意図しない変更を加えてしまう可能性があります。
これは先ほど説明した「シャローコピー」のような、コピー先に変更を加えるとコピー元にも変更が反映されてしまうような場合です。
原本またはコピーのどちらかを書き換えようとしたときに、それを検出し、その時点ではじめて新たな空き領域を探して割り当て、コピーを実行する。
これが「書き換え時にコピーする」、すなわちコピーオンライト (Copy-On-Write) の基本的な形態となります。
リファレンスカウントの原理
PHPでは、「プログラムが動的に確保したメモリ領域のうち、不要になった領域を自動的に解放する」いわゆるガベージコレクションが機能として存在します。
PHPのガベージコレクションでは「参照カウント(リファレンスカウント)法」という方式で管理されています。
zvalコンテナ
PHPの変数は「zval」と呼ばれる特別なコンテナに値と型と共に保管されており、変数の型と値の他に、情報の追加ビットを2つ含んでいます。<zvalコンテナに含まれる情報>
①変数の型
②値
③is_ref
④refcount
【is_ref】
「is_ref」は、変数が参照集合の一部かどうかを示すブール値の情報で、これによりPHPエンジンは通常の変数と参照を区別することができます。
PHPでは参照を使うことができるため、zvalコンテナは内部的なリファレンスカウント機構を持っており、メモリ使用状況を最適化します。
デフォルトでfalseにセットされます。
【refcount】
「refcount」は、この1つのzvalコンテナをどれだけ多くの変数名が指すかを表します。
具体的に挙動を確認していきます。
$a = "new string";
xdebug_debug_zval('a');
// 出力結果
// a: (refcount=1, is_ref=0)='new string'
・$a = "new string";というコードが実行されると、新しい変数名$aが現在のスコープで作成され、この$a変数には値が "new string"であり、型がstringである新しい変数コンテナが作成されます。
・この変数コンテナには、参照集合の一部かどうかを示すis_refという情報のビットが含まれていますが、この時点ではユーザーランド参照(リファレンスによる代入:&$変数)が作成されていないため、is_refはデフォルトでfalseにセットされます。
また、この変数コンテナを利用するシンボル(変数名:$a)が1つだけあるため、refcountは1に設定されます。
refcountの減少でも同様に、以下のようにリンクが解除されることでrefcountの値は変化します。
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset( $c );
xdebug_debug_zval( 'a' );
// 出力結果
// a: (refcount=3, is_ref=0)='new string'
// a: (refcount=2, is_ref=0)='new string'
// a: (refcount=1, is_ref=0)='new string'
配列型の場合は少し複雑で、それらのプロパティをそれら自身のシンボルテーブルに保管します。
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
/** 出力結果
* a: (refcount=1, is_ref=0)=array (
* 'meaning' => (refcount=1, is_ref=0)='life',
* 'number' => (refcount=1, is_ref=0)=42
* )
**/
イメージは以下のようになります。
引用元:PHP公式リファレンス
まとめ
PHPでは、変数の代入は値渡し(ディープコピー)が規定ですが、オブジェクト変数だけは例外的に参照渡し(リファレンス渡し/シャローコピー)が規定となっている。
シャローコピー
見ている元データは同じですが、データ自体をコピーしているわけではありません。
ディープコピー
新しい変数を作り、元データの実体そのものもコピーします。
clone
clone命令の規定の挙動はシャローコピーです。
コピーオンライト
原本またはコピーのどちらかを書き換えようとしたときに、それを検出し、その時点ではじめて新たな空き領域を探して割り当て、コピーを実行する。
リファレンスカウントの原理
<zvalコンテナに含まれる情報>
①変数の型
②値
③is_ref
④refcount