ツイート
シェア
LINEで送る
B! はてぶでブックマーク
Pocketでブックマーク
RSSフィード

PHP, クラスオブジェクトのディープコピー。cloneキーワードを付けるだけじゃダメ!

php
イラストダウンロードサイト【イラストAC】
の画像をもとに加工しています。

PHPのオブジェクトの代入(=)はシャローコピーなので、厳密にはコピーじゃありません。そこでやらないといけないのがオブジェクトのクローン作成。

PHPではcloneキーワードを付けるだけのように見えるんですが、やることは他にもあります。

そこはやっぱりJavaと似ている。

シャローコピーのとんでもな動き

まずは、Personというクラスを使って、『たろう』と『じろう』のオブジェクトを作ってみます。

<?php

class Person {
	public $name;
}

$taro = new Person();
$taro->name = "taro";

$jiro = $taro;
$jiro->name = "jiro";

var_dump($taro);
var_dump($jiro);

『何か問題でも?』と思う人がいるかもしれません。でもこれ大問題です。var_dump()の結果はこうなります。

実行結果
object(Person)#1 (1) {
  ["name"]=>
  string(4) "jiro"
}
object(Person)#1 (1) {
  ["name"]=>
  string(4) "jiro"
}

『じろう』を『たろう』からコピーして作ったつもりが、2つとも『じろう』になっちゃいました。

オブジェクトの代入(=)はシャローコピーで、実際はコピーじゃないから。

これを解決するのがオブジェクトの代入をディープコピーにするcloneキーワードです。

cloneキーワードだけを付けるとコピーは完ぺきじゃない

オブジェクトはイコール(=)で代入するだけだとダメです。1行だけ修正しましょう。

<?php

class Person {
	public $name;
}

$taro = new Person();
$taro->name = "taro";

$jiro = clone $taro;
$jiro->name = "jiro";

var_dump($taro);
var_dump($jiro);

10行目を、cloneキーワードを使った代入(= clone)に変更するだけ。結果を見てみましょう。

実行結果
object(Person)#1 (1) {
  ["name"]=>
  string(4) "taro"
}
object(Person)#2 (1) {
  ["name"]=>
  string(4) "jiro"
}

上手くいきました。

『オブジェクトのディープコピーってかんたんだね?』と言いたいところですが、これでは中途半端です。

次のコードで中途半端さを見てみましょう。

<?php

class Person {
	public $name;
	public $child;
}

class Child {
	public $name;
}

$taro = new Person();
$taro->name = "taro";
$child = new Child();
$child->name = "hana";
$taro->child = $child;

$jiro = clone $taro;
$jiro->name = "jiro";
$jiro->child->name = "hikari";

var_dump($taro);
var_dump($jiro);

PersonクラスのプロパティでChildクラスをもたせました。実行結果はこうなります。

実行結果
object(Person)#1 (2) {
  ["name"]=>
  string(4) "taro"
  ["child"]=>
  object(Child)#2 (1) {
    ["name"]=>
    string(6) "hikari"
  }
}
object(Person)#3 (2) {
  ["name"]=>
  string(4) "jiro"
  ["child"]=>
  object(Child)#2 (1) {
    ["name"]=>
    string(6) "hikari"
  }
}

『じろう』の子どもを変更したつもりなのに『たろう』の子どもも変わっちゃいました。

cloneキーワードを使うだけでは、オブジェクトが持っている内部のオブジェクトまではディープコピーしてくれません。

var_dump()のオブジェクトのIDは2つとも "#2" で同じ。これは『たろう』と『じろう』は $child を共有していることを意味します。

なんか複雑な家庭になっちゃった。

完ぺきなディープコピーは __clone()メソッドを使う

Personクラス内のChildオブジェクトはコピーできませんでした。それを実現するのが __clone() メソッドです。

Personクラスを修正します。

<?php

class Person {
	public $name;
	public $child;
	
	public function __clone() {
		$this->child = clone $this->child;
	}
}

// 以下同じ。
実行結果
object(Person)#1 (2) {
  ["name"]=>
  string(4) "taro"
  ["child"]=>
  object(Child)#2 (1) {
    ["name"]=>
    string(4) "hana"
  }
}
object(Person)#3 (2) {
  ["name"]=>
  string(4) "jiro"
  ["child"]=>
  object(Child)#4 (1) {
    ["name"]=>
    string(6) "hikari"
  }
}

__clone() は、cloneキーワードを使ったオブジェクトの代入のときだけコールされる特殊なメソッドです。

(PHPの特殊メソッドは頭に "__" が付く。__construct() など。)

public は無くてもいいですが、private, protected ではダメ。要は publicメソッドでないとエラーになります。

このメソッド内で、$child のディープコピーを行います。ここでも cloneキーワードを使いました。

『Childクラスの cloneキーワード付き代入をしたら Childクラスにも__clone()が必要では?』

と思うはず。厳密には必要です。

ただ、Childクラスのプロパティにはオブジェクト値を代入するものがありません。

その場合、__clone() は不要。メソッド内で書く処理がないから。説明は後述。

clone はオブジェクトのプロパティのコピー

cloneキーワードを使ったオブジェクトのコピーは、オブジェクトのプロパティ値のコピーです。

それだけと言っても過言じゃない。

__clone() では、プロパティ値のディープコピーをする処理だけを書きます。

初期値設定とかそれ以外の処理を書けなくもないがやめるべき。

cloneキーワードを使うところではクローンを作ると思っている。

それ以外のことをするのを想定しない。

また、__clone()内では、オブジェクト型以外のプロパティのコピー処理はいらないです。

int, boolean, 配列, 文字列型など、プリミティブ型のプロパティは clone 代入した時点でディープコピーされるから。上記サンプルでChildクラスに__clone()を書かなかったのもそう。

このような仕様はPHPに限ったことじゃありません。= で代入するとき、プリミティブ型はディープコピーでオブジェクト型はシャローコピーになるのがオブジェクト指向言語の特長。

このへんの仕様はJavaとかなり酷似している。

Javaでは、プロパティのディープコピー処理をクラスのclone()メソッドで書くようになっている。

ただし、Javaにはcloneキーワードはない。直接オブジェクトのclone()をコールする。

ちょっとした応用。__clone()がないときは。

サードパーティ製のプログラムを使ってて、そのクラスの__clone()がちゃんと書いてないときどうしようってなると思います。

そういうときは、親オブジェクトのクローンを作って、そのプロパティを個別にクローン作成してしまえば同じことが出来る。

<?php

class Person {
	public $name;
	public $child;
}

class Child {
	public $name;
}

$taro = new Person();
$taro->name = "taro";
$child = new Child();
$child->name = "hana";
$taro->child = $child;

$jiro = clone $taro;
$jiro->name = "jiro";
$jiro->child = clone $taro->child;
$jiro->child->name = "hikari";

var_dump($taro);
var_dump($jiro);
実行結果
object(Person)#1 (2) {
  ["name"]=>
  string(4) "taro"
  ["child"]=>
  object(Child)#2 (1) {
    ["name"]=>
    string(4) "hana"
  }
}
object(Person)#3 (2) {
  ["name"]=>
  string(4) "jiro"
  ["child"]=>
  object(Child)#4 (1) {
    ["name"]=>
    string(6) "hikari"
  }
}

ただ、クラスのプロパティで数珠つなぎのようにオブジェクトがつながっているとき面倒。それなら__clone()を追加した子クラスを自作しましょう。

class Person {
	public $name;
	public $child;
}

class Person2 extends Person {
	public function __clone() {
		$this->child = clone $this->child;
	}
}

普通は子クラスを作るでしょうね? オブジェクト指向ってそういうものだから。

PHP公式ドキュメント

オブジェクトのクローン作成

前の投稿
PHP, クラスオブジェクトの代入(=)は全くの別物。データコピーだと思ってたら大爆死。
PHP, globalキーワード付き変数は何なのか? $GLOBALS と何が違うのか?
次の投稿
コメントを残す

*