ここまでインスタンスのコピーを見てきましたが、実はまだ、問題が隠れています。
まずは、問題のあるコードから見ていきましょう。
<?php // class hoge { public $i_; public $obj_; } class foo { public $j_; } // $obj = new hoge(); $obj->i_ = 1; $obj->obj_ = new foo(); $obj->obj_->j_ = 20; $obj2 = clone $obj; // 代入ではなくcloneを使う var_dump($obj); var_dump($obj2); // $obj2->i_ = 999; // 片方だけ値を変更 $obj2->obj_->j_ = 999; var_dump($obj); var_dump($obj2);
object(hoge)#1 (2) { ["i_"]=> int(1) ["obj_"]=> object(foo)#2 (1) { ["j_"]=> int(20) } } object(hoge)#3 (2) { ["i_"]=> int(1) ["obj_"]=> object(foo)#2 (1) { ["j_"]=> int(20) } } object(hoge)#1 (2) { ["i_"]=> int(1) ["obj_"]=> object(foo)#2 (1) { ["j_"]=> int(999) } } object(hoge)#3 (2) { ["i_"]=> int(999) ["obj_"]=> object(foo)#2 (1) { ["j_"]=> int(999) } }
$objには、#1、$obj2には#3のインスタンスが入っているので、プロパティ$i_の値は適切に分離ができています。しかし、プロパティ$obj_の中にあるインスタンスは、#1の中も#3の中も、どちらも#2のインスタンスが入ってしまっています(結果9の25、34行目が同じ)。「$obj2->obj_->j_ = 999;」(リスト9の20行目)で片方のインスタンスだけ値を変更したいのに、#1の中の$obj_も、引きずられるように値が変更されてしまっているのです。
言い方を変えると、#1は適切にコピー(clone)されていますが、#1の中にあるインスタンスは「コピー(clone)されていない、中途半端な状態になっている」といえます。
こういった状態を「浅いコピー(シャローコピー:Shallow Copy)」と言って、オブジェクト指向プログラミングでは、言語を問わず出て来る概念です。もちろん「意図してこの状態にしている」のであればいいのですが、大抵の場合はそうではないので、より適切に「深いコピー(ディープコピー:Deep Copy)」を行う必要があることが多いといわれています。
PHPでは、こんなときにもマジックメソッドを用います。「__clone」というマジックメソッドは「cloneされた直後」に暗黙的に呼び出されるので、この__cloneを使って「深いコピー」を実装します。
では、実装例を見ていきましょう。
<?php // class hoge { // public function __clone() { echo "call __clone()\n"; $this->obj_ = clone $this->obj_; // プロパティのうち「インスタンス型のもの」は全てcloneしておく } // public $i_; public $obj_; } class foo { public $j_; } // $obj = new hoge(); $obj->i_ = 1; $obj->obj_ = new foo(); $obj->obj_->j_ = 20; $obj2 = clone $obj; // 代入ではなくcloneを使う var_dump($obj); var_dump($obj2); // $obj2->i_ = 999; // 片方だけ値を変更 $obj2->obj_->j_ = 999; var_dump($obj); var_dump($obj2);
call __clone() object(hoge)#1 (2) { ["i_"]=> int(1) ["obj_"]=> object(foo)#2 (1) { ["j_"]=> int(20) } } object(hoge)#3 (2) { ["i_"]=> int(1) ["obj_"]=> object(foo)#4 (1) { ["j_"]=> int(20) } } object(hoge)#1 (2) { ["i_"]=> int(1) ["obj_"]=> object(foo)#2 (1) { ["j_"]=> int(20) } } object(hoge)#3 (2) { ["i_"]=> int(999) ["obj_"]=> object(foo)#4 (1) { ["j_"]=> int(999) } }結果10
__cloneが呼び出されていること。その中でcloneした結果として、#1の中のobj_と、#3の中のobj_の中にあるインスタンスが、それぞれ「別のもの(#2と#4)」になっていること(結果10の26、35行目が異なる)。その結果として「$obj2->obj_->j_ = 999;」(リスト10の26行目)の操作が、$objに影響していないことが分かると思います。
「インスタンスをコピーしたい」というケースはあまりありません。しかし「必要になった」ときに、こういった知識の有無が業務の速度に大きく影響することがあるので、「浅いコピー」「深いコピー」という考え方を、しっかりと理解しておくとよいでしょう。
全6回にわたって、「PHPにおけるオブジェクト指向」の「比較的よくある」話をいくつかしました。その中でも「他言語でも一般的に通用する内容」「PHPに比較的固有なお話」がありました。この2つは、どちらも大切でバランス良く学習していくといいでしょう。特に、プログラミング言語固有ではない「プログラミングの知識」は中級プログラマーの頃に意識する必要があると思います。
またどこかでチャンスがありましたら、PHPの「少しだけ濃い、深い辺り」に踏み込んだ記事を皆さまにお届けできればと思っております。それでは、全6回をご愛読いただきありがとうございました。本稿がプログラマーの皆さまの参考になれば、望外の幸いです。
Copyright © ITmedia, Inc. All Rights Reserved.