PHPに限らず、プログラミングのイコール(=)は値の代入、言いかえれば、右辺から左辺へのコピーです。
ただ、クラスのオブジェクトではちがう。コピーですらないので、知らないととんでもない間違いを犯します。
これはPHPでも同じ。オブジェクト指向プログラミング特有の現象。
まずは、かんたんな値代入、コピーを見てみましょう。
<?php
// 値の代入
$test1 = "default";
// 値のコピー
$test2 = $test1;
var_dump($test1);
var_dump($test2);
// コピー先の編集
$test2 = $test2 . " -> change";
var_dump($test1);
var_dump($test2);
string(7) "default"
string(7) "default"
string(7) "default"
string(17) "default -> change"
かんたんすぎて、結果に疑問のもちようがありません。
今回のテーマに関わるので、コピーした値を編集しました。今度はこれをクラスのオブジェクトでしてみます。
<?php
class Test {
public $status = "default";
}
// 値の代入
$test1 = new Test();
// 値のコピー
$test2 = $test1;
var_dump($test1);
var_dump($test2);
// コピー先の編集
$test2->status = $test2->status . " -> change";
var_dump($test1);
var_dump($test2);
何も問題ないように見えますが大あり。これに気づいている人は今回のテーマのことをよく分かっています。ここから先は読む必要ありません。
問題に気づかない人は、実行結果を見て下さい。
object(Test)#1 (1) {
["status"]=>
string(7) "default"
}
object(Test)#1 (1) {
["status"]=>
string(7) "default"
}
object(Test)#1 (1) {
["status"]=>
string(17) "default -> change"
}
object(Test)#1 (1) {
["status"]=>
string(17) "default -> change"
}
値を変えたのは $test2 ですよね? でも、変更結果が $test1 にも反映されてます。これが大問題の原因。
オブジェクトのイコール(=)は、コピーではなくエイリアス変数の作成
オブジェクトのイコール(=)で値代入は問題ないですが、変数を変数に代入するとコピーではなくなります。
$test1 と $test2 の見ているデータは同じ。だから一方の変更点がもう一方にまで影響します。
この現象のことをシャローコピーといいます。シャローコピーはコピーと言いながらコピーではありません。くわしくはこちらをどうぞ。
これはPHPに限らず、オブジェクト指向プログラミング特有の仕様です。Javaでも同じ。
プリミティブ型はディープコピーになり、オブジェクト型はシャローコピーになる。ここをきちんと区別できないととんでもないバグを作ります。
オブジェクト指向は苦手っていう人はここでつまずくんじゃないかな?
でも、これを理解するだけでオブジェクト指向プログラミングの半分以上は理解できてると言っても過言じゃありません。
よく、『オブジェクトは参照渡し』と言われますが、今回のテーマを端的に表した言葉。
オブジェクトのコピーはクローン作成を明示する
これだと、オブジェクトの変数名だけが変わってるだけでイコールで結ぶ必要ないですよね?
オブジェクトのコピーには、明確にコピーしますよという明示する必要があり、その方法が用意されています。
さっきのサンプルを修正します。
<?php
class Test {
public $status = "default";
}
// 値の代入
$test1 = new Test();
// 値のコピー
$test2 = clone $test1;
var_dump($test1);
var_dump($test2);
// コピー先の編集
$test2->status = $test2->status . " -> change";
var_dump($test1);
var_dump($test2);
11行目を見て下さい。イコールの右隣りにcloneキーワードを追加して、オブジェクトをコピーすることを明示しました。
結果はこうなります。
object(Test)#1 (1) {
["status"]=>
string(7) "default"
}
object(Test)#2 (1) {
["status"]=>
string(7) "default"
}
object(Test)#1 (1) {
["status"]=>
string(7) "default"
}
object(Test)#2 (1) {
["status"]=>
string(17) "default -> change"
}
変数のエイリアスではなくコピーできてます。
var_dump()結果の、オブジェクトのID(#[数値])に注目。
$test1 と $test2 でIDが変わってコピーされていることが分かる。
前のサンプルは2つとも #1 で、同じデータを見ていることが確認できるので、ちがいは一目瞭然。
これでOKと行きたいところですが、cloneキーワードには注意点があり、このままでは中途半端です。ここでは、『完ぺきなオブジェクトのコピーにはクラスに処理の追加が必要』にとどめておきます。
続きはこちらをどうぞ。
オブジェクト指向はかんたんにコピーされては困る
オブジェクト指向プログラミングではクラスをつなげて処理を作っていくんですが、いくつものオブジェクト(インスタンス)を作成する手前、かんたんにコピーを作られては困る仕様になっています。
オブジェクト指向プログラムはメモリ消費が大きいので、ひとつのインスタンスを使い回す。
クラスはそもそもデータの型を意味するので、複数のインスタンスを作る意味がまったくありません。
(クラスは同じでもプロパティ値がちがうだけで別の型ととらえる。)
だから、オブジェクト指向はコピー禁止がデフォルト。コピーを作るには明確な意味と意思(明示)が必要です。
PHP公式ドキュメント