我认为需要注意的是 __clone 不是重写。如示例所示,正常的克隆过程始终发生,并且 __clone 方法的责任是“修复”由它执行的任何“错误”操作。
创建具有完全复制属性的对象副本并不总是想要的行为。一个需要使用复制构造函数的典型例子是,如果有一个对象代表一个 GTK 窗口,并且该对象包含了这个 GTK 窗口的资源,当你创建副本时,你可能想要创建一个具有相同属性的新窗口,并让新对象包含新窗口的资源。另一个例子是,如果你的对象包含对另一个对象的引用,它使用该对象,当你复制父对象时,你想创建一个该对象的新的实例,以便副本拥有它自己的独立副本。
使用 clone
关键字创建对象的副本(如果可能,它将调用对象的 __clone() 方法)。
$copy_of_object = clone $object;
当克隆对象时,PHP 将对对象的所有属性执行浅拷贝。任何对其他变量的引用的属性都将保持引用。
克隆完成后,如果定义了 __clone() 方法,那么新创建对象的 __clone() 方法将被调用,以允许任何需要更改的必要属性。
示例 #1 克隆一个对象
<?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);
?>
上面的示例将输出
Original Object: MyCloneable Object ( [object1] => SubObject Object ( [instance] => 1 ) [object2] => SubObject Object ( [instance] => 2 ) ) Cloned Object: MyCloneable Object ( [object1] => SubObject Object ( [instance] => 3 ) [object2] => SubObject Object ( [instance] => 2 ) )
可以在单个表达式中访问新克隆对象的成员
示例 #2 访问新克隆对象的成员
<?php
$dateTime = new DateTime();
echo (clone $dateTime)->format('Y');
?>
上面的示例将输出类似于
2016
我认为需要注意的是 __clone 不是重写。如示例所示,正常的克隆过程始终发生,并且 __clone 方法的责任是“修复”由它执行的任何“错误”操作。
这是我编写的用于测试 clone 行为的测试脚本,当我的类中包含具有原始值的数组时 - 作为 jeffrey at whinger dot nl 下面注释的附加测试
<pre>
<?php
class MyClass {
private $myArray = array();
function pushSomethingToArray($var) {
array_push($this->myArray, $var);
}
function getArray() {
return $this->myArray;
}
}
// 将一些值推送到 Mainclass 的 myArray
$myObj = new MyClass();
$myObj->pushSomethingToArray('blue');
$myObj->pushSomethingToArray('orange');
$myObjClone = clone $myObj;
$myObj->pushSomethingToArray('pink');
// 测试
print_r($myObj->getArray()); //Array([0] => blue,[1] => orange,[2] => pink)
print_r($myObjClone->getArray());//Array([0] => blue,[1] => orange)
// 因此数组被克隆
?>
</pre>
我遇到了相同的问题,在一个对象内部有一个对象数组,我希望克隆它们都指向相同的对象。但是,我同意序列化数据不是答案。它相对简单,真的
public function __clone() {
foreach ($this->varName as &$a) {
foreach ($a as &$b) {
$b = clone $b;
}
}
}
注意,我使用的是多维数组,并且没有使用 Key=>Value 对系统,但是基本点是,如果你使用 foreach,你需要指定复制数据可以通过引用访问。
我遇到的另一个问题:与 __construct 和 __desctruct 一样,你必须在子类的 __clone() 函数内部自己调用 parent::__clone()。手册在这方面有点让我误入歧途:“对象的 __clone() 方法不能直接调用。”
以下是一些我们在 Last.fm 遇到的克隆和引用问题。
1. PHP 将变量视为“值类型”或“引用类型”,其中差异应该是透明的。对象克隆是少数情况下差异会产生重大影响的之一。我不知道有任何编程方法可以判断一个变量从本质上说是值类型还是引用类型。但是,有一种非编程方法可以判断对象属性是值类型还是引用类型
<?php
class A { var $p; }
$a = new A;
$a->p = 'Hello'; // $a->p 是值类型
var_dump($a);
/*
object(A)#1 (1) {
["p"]=>
string(5) "Hello" // <-- 无 &
}
*/
$ref =& $a->p; // 注意,这会将 $a->p 转换为引用类型!
var_dump($a);
/*
object(A)#1 (1) {
["p"]=>
&string(5) "Hello" // <-- 注意 &,这表示它是一个引用。
}
*/
?>
2. 取消除一个之外所有引用的设置将把剩余的引用转换回值。从前面的例子继续
<?php
unset($ref);
var_dump($a);
/*
object(A)#1 (1) {
["p"]=>
string(5) "Hello"
}
*/
?>
我将此解释为引用计数从 2 直接跳到 0。但是…
2. 确实可以创建一个引用计数为 1 的引用,即在没有额外引用的情况下将属性从值类型转换为引用类型。您只需声明它引用自身即可。这非常奇特,但它确实有效。这引出了这样一个观察:尽管手册中指出“任何对其他变量的引用的属性都将保持为引用”,但这并不完全正确。任何变量都是引用,即使是*自身*(不一定是其他变量),也会通过引用复制,而不是通过值复制。
这是一个演示的例子
<?php
class ByVal
{
var $prop;
}
class ByRef
{
var $prop;
function __construct() { $this->prop =& $this->prop; }
}
$a = new ByVal;
$a->prop = 1;
$b = clone $a;
$b->prop = 2; // $a->prop 仍然为 1
$a = new ByRef;
$a->prop = 1;
$b = clone $a;
$b->prop = 2; // $a->prop 现在为 2
?>
这是一个关于克隆问题的基本例子。如果我们在 getClassB 方法中使用克隆。返回值将与 new B() 的结果相同。但是,如果我们不使用克隆,我们就可以影响 B::$varA。
class A
{
protected $classB;
public function __construct(){
$this->classB = new B();
}
public function getClassB()
{
return clone $this->classB;
}
}
class B
{
protected $varA = 2;
public function getVarA()
{
return $this->varA;
}
public function setVarA()
{
$this->varA = 3;
}
}
$a = new A();
$classB = $a->getClassB();
$classB->setVarA();
echo $a->getClassB()->getVarA() . PHP_EOL;// 使用克隆 -> 2,不使用克隆则返回 -> 3
echo $classB->getVarA() . PHP_EOL; // 始终返回 3
不用说,如果你有循环引用,比如对象 A 的属性引用对象 B,而 B 的属性引用 A(或比这更间接的循环),那么你会很高兴克隆没有自动进行深度复制!
<?php
class Foo
{
var $that;
function __clone()
{
$this->that = clone $this->that;
}
}
$a = new Foo;
$b = new Foo;
$a->that = $b;
$b->that = $a;
$c = clone $a;
echo 'What happened?';
var_dump($c);
此基类会自动递归克隆对象类型或对象类型数组值的属性。只需从这个基类继承你自己的类。
<?php
class clone_base
{
public function __clone()
{
$object_vars = get_object_vars($this);
foreach ($object_vars as $attr_name => $attr_value)
{
if (is_object($this->$attr_name))
{
$this->$attr_name = clone $this->$attr_name;
}
else if (is_array($this->$attr_name))
{
// 注意:这只会复制一维数组
foreach ($this->$attr_name as &$attr_array_value)
{
if (is_object($attr_array_value))
{
$attr_array_value = clone $attr_array_value;
}
unset($attr_array_value);
}
}
}
}
}
}
?>
例子
<?php
class foo extends clone_base
{
public $attr = "Hello";
public $b = null;
public $attr2 = array();
public function __construct()
{
$this->b = new bar("World");
$this->attr2[] = new bar("What's");
$this->attr2[] = new bar("up?");
}
}
class bar extends clone_base
{
public $attr;
public function __construct($attr_value)
{
$this->attr = $attr_value;
}
}
echo "<pre>";
$f1 = new foo();
$f2 = clone $f1;
$f2->attr = "James";
$f2->b->attr = "Bond";
$f2->attr2[0]->attr = "Agent";
$f2->attr2[1]->attr = "007";
echo "f1.attr = " . $f1->attr . "\n";
echo "f1.b.attr = " . $f1->b->attr . "\n";
echo "f1.attr2[0] = " . $f1->attr2[0]->attr . "\n";
echo "f1.attr2[1] = " . $f1->attr2[1]->attr . "\n";
echo "\n";
echo "f2.attr = " . $f2->attr . "\n";
echo "f2.b.attr = " . $f2->b->attr . "\n";
echo "f2.attr2[0] = " . $f2->attr2[0]->attr . "\n";
echo "f2.attr2[1] = " . $f2->attr2[1]->attr . "\n";
?>
有没有办法知道一个对象被克隆了多少次?我认为这是正确的。
<?php
class Classe {
public static $howManyClones = 0;
public function __clone() {
++static::$howManyClones;
}
public static function howManyClones() {
return static::$howManyClones;
}
public function __destruct() {
--static::$howManyClones;
}
}
$a = new Classe;
$b = clone $a;
$c = clone $b;
$d = clone $c;
echo 'Clones:' . Classe::howManyClones() . PHP_EOL;
unset($d);
echo 'Clones:' . Classe::howManyClones() . PHP_EOL;
<?php
class Foo
{
private $bar = 1;
public function get()
{
$x = clone $this;
return $x->bar;
}
}
// 不会抛出异常。
// 即使在克隆上作为外部调用,Foo::$bar 属性在内部也是可见的
print (new Foo)->get();
手册中清楚地描述了克隆过程的机制。
- 首先,浅拷贝:引用属性将保留引用(指向同一个目标/变量)。
- 然后,根据要求更改内容/属性(调用用户定义的 __clone 方法)。
为了说明这个过程,以下示例代码似乎更好,可以与原始版本进行比较。
class SubObject
{
static $num_cons = 0;
static $num_clone = 0;
public $construct_value;
public $clone_value;
public function __construct() {
$this->construct_value = ++self::$num_cons;
}
public function __clone() {
$this->clone_value = ++self::$num_clone;
}
}
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("Original Object:\n");
print_r($obj);
echo '<br>';
print("Cloned Object:\n");
print_r($obj2);
==================
输出如下
Original Object
MyCloneable Object
(
[object1] => SubObject Object
(
[construct_value] => 1
[clone_value] =>
)
[object2] => SubObject Object
(
[construct_value] => 2
[clone_value] =>
)
)
<br>Cloned Object
MyCloneable Object
(
[object1] => SubObject Object
(
[construct_value] => 1
[clone_value] => 1
)
[object2] => SubObject Object
(
[construct_value] => 2
[clone_value] =>
)
)
想要进行深度克隆,但又不想太麻烦吗?
<?php
function __clone() {
foreach($this as $key => $val) {
if(is_object($val)||(is_array($val))){
$this->{$key} = unserialize(serialize($val));
}
}
}
?>
这将确保任何对象或数组(可能包含对象)都将被克隆,而无需使用递归或其他支持方法。
[由 danbrown AT php DOT net 编辑:几乎完全相同的功能由(david ashe AT metabin)于 2008 年 12 月 2 日 10:18 贡献。
<?php
function __clone(){
foreach($this as $name => $value){
if(gettype($value)=='object'){
$this->$name= clone($this->$name);
}
}
}
?>
归功于应得的人。~DPB]
[由 cmb AT php DOT net 编辑:后一个函数无法对对象数组进行深度复制,并且可能导致无限递归。]
@DPB
我认为这两个函数并不完全相同。序列化然后反序列化的方式是我在其他语言中进行深度克隆的方式(绕过任何奇怪的克隆函数行为,并确保你拥有对象的无字符串副本)。