2024年PHP日本大会

对象克隆

创建具有完全复制属性的对象副本并不总是想要的行为。一个需要复制构造函数的好例子是,如果您有一个表示GTK窗口的对象,并且该对象持有此GTK窗口的资源,那么当您创建副本时,您可能希望创建一个具有相同属性的新窗口,并让新对象持有新窗口的资源。另一个例子是,如果您的对象持有对它使用的另一个对象的引用,并且当您复制父对象时,您希望创建一个此其他对象的新实例,以便副本拥有它自己的单独副本。

对象副本是使用clone关键字创建的(如果可能,它会调用对象的__clone()方法)。

$copy_of_object = clone $object;

克隆对象时,PHP将对所有对象的属性执行浅复制。任何对其他变量的引用的属性都将保持为引用。

__clone(): void

克隆完成后,如果定义了__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
添加注释

用户贡献的注释 11条注释

jorge dot villalobos at gmail dot com
19年前
我认为需要注意的是__clone不是重写。如示例所示,正常的克隆过程始终发生,__clone方法负责“纠正”任何由其执行的“错误”操作。
jojor at gmx dot net
14年前
这是我编写的测试脚本,用于测试当我的类中包含具有原始值的数组时克隆的行为 - 作为对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()); // 数组([0] => blue,[1] => orange,[2] => pink)
print_r($myObjClone->getArray());// 数组([0] => blue,[1] => orange)
// 所以数组被克隆了

?>
</pre>
MakariVerslund at gmail dot com
17年前
我遇到了同样的问题,即对象内有一个对象数组,我想克隆的所有对象都指向相同的对象。但是,我同意序列化数据不是答案。它相对简单,真的

public function __clone() {
foreach ($this->varName as &$a) {
foreach ($a as &$b) {
$b = clone $b;
}
}
}

请注意,我正在使用多维数组,并且我没有使用Key=>Value对系统,但基本上,重点是,如果您使用foreach,则需要指定要按引用访问复制的数据。
emile at webflow dot nl
14年前
我遇到的另一个问题:像__construct和__desctruct一样,您必须从子类的__clone()函数内部自行调用parent::__clone()。手册在这里有点让我误入歧途:“不能直接调用对象的__clone()方法。”
ben at last dot fm
15年前
以下是一些我们在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

?>
tolgakaragol at gmail dot com
5年前
这是一个关于克隆问题的基本示例。如果我们在getClassB方法中使用克隆,返回值将与new B()的结果相同。但是,如果我们不使用克隆,我们可以影响B::$varA。

类 A
{
protected $classB;

public function __construct(){
$this->classB = new B();
}

public function getClassB()
{
return clone $this->classB;
}
}

类 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
Hayley Watson
16年前
毋庸置疑的是,如果你有循环引用,其中对象 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
'发生了什么?';
var_dump($c);
stanislav dot eckert at vizson dot de
9年前
这个基类会自动递归地克隆对象类型属性或对象类型数组值。只需将你自己的类继承自这个基类。

<?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";
?>
fabio at naoimporta dot com
8年前
可以知道一个对象创建了多少个克隆。我认为这是正确的。

<?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
'克隆数:' . Classe::howManyClones() . PHP_EOL;

unset(
$d);

echo
'克隆数:' . Classe::howManyClones() . PHP_EOL;
flaviu dot chelaru at gmail dot com
6年前
<?php

class Foo
{
private
$bar = 1;

public function
get()
{
$x = clone $this;
return
$x->bar;
}
}
// 不会抛出异常。
// 即使在克隆对象上作为外部调用,Foo::$bar 属性在内部也是可见的
print (new Foo)->get();
yinzw at chuchujie dot com
8年前
手册中清楚地描述了克隆过程的机制
- 首先,浅拷贝:引用的属性将保持引用(引用相同的目标/变量)
- 然后,根据请求更改内容/属性(调用用户定义的 __clone 方法)。

为了说明这个过程,与原始版本相比,下面的示例代码似乎更好

类 SubObject
{
静态 $num_cons = 0;
静态 $num_clone = 0;

公共 $construct_value;
公共 $clone_value;

公共函数 __construct() {
$this->construct_value = ++self::$num_cons;
}

public function __clone() {
$this->clone_value = ++self::$num_clone;
}
}

类 MyCloneable
{
公共 $object1;
公共 $object2;

函数 __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);
echo '<br>';
print("克隆对象:\n");
print_r($obj2);

==================

输出如下

原始对象
MyCloneable 对象
(
[object1] => SubObject 对象
(
[construct_value] => 1
[clone_value] =>
)

[object2] => SubObject 对象
(
[construct_value] => 2
[clone_value] =>
)

)
<br>克隆对象
MyCloneable 对象
(
[object1] => SubObject 对象
(
[construct_value] => 1
[clone_value] => 1
)

[object2] => SubObject 对象
(
[construct_value] => 2
[clone_value] =>
)

)
To Top