如何在PHP中创建一个对象的副本?

207

看起来在PHP中对象是通过引用传递的。即使赋值运算符似乎也没有创建对象的副本。

这里有一个简单而人为的证明:

<?php

class A {
    public $b;
}


function set_b($obj) { $obj->b = "after"; }

$a = new A();
$a->b = "before";
$c = $a; //i would especially expect this to create a copy.

set_b($a);

print $a->b; //i would expect this to show 'before'
print $c->b; //i would ESPECIALLY expect this to show 'before'

?>

无论是打印情况1还是情况2,我都获得了“after”。

那么,如何将$a按值而不是按引用传递给set_b()


3
很少有情况需要这种行为。因此,如果您经常使用它,那么也许您编写代码的方式存在更根本性的问题? - troelskn
1
还没有需要使用它的必要。 - Nick Stinemates
(object) ((array) $objectA) 可能会以更好的性能给您相同的期望结果,而不是使用 clone $objectAnew stdClass - Binyamin
“即使赋值运算符似乎也没有创建对象的副本。” - 我希望不是这样!如果是这样,结果将不再是面向对象语言(在所有实际目的上)。 - ToolmakerSteve
9个回答

335
在PHP 5+中,对象是按引用传递的。在PHP 4中,它们是按值传递的(这就是为什么它具有运行时引用传递,后来被弃用)。您可以在PHP5中使用“clone”运算符复制对象。
$objectB = clone $objectA;

另外,只有对象是按引用传递的,不像你在问题中说的一样所有的东西都是这样...


想要对正在阅读此内容的任何人补充一下,克隆将保留对原始对象的引用。使用克隆对象运行 MySQL 查询可能会产生不可预测的结果,因为执行可能不按线性方式进行。 - Ælex
24
为了纠正一个常见误解(我认为甚至PHP文档都错了!),PHP 5 的对象不是“按引用传递”的。就像Java一样,它们有额外的间接层 —— 变量指向“对象指针”,而该指针指向对象。因此,两个变量可以指向同一个对象,而不必是对同一值的引用。可以从这个例子中看出:$a = new stdClass; $b =& $a; $a = 42; var_export($b); 这里 $b 是对变量 $a 的引用;如果您将 =& 替换为普通的 =,它就不是引用,仍然指向原始对象。 - IMSoP
运行时按引用传递是一个不好的想法,因为它使得函数调用的效果取决于函数的实现,而不是规范。这与按值传递是默认值无关。 - Oswald
1
@Alex,你能详细说明一下你的评论吗?(可以在这里或其他地方。)我认为你的观点有点不清楚。 - Chris Middleton
1
@Ælex - 关于“克隆将保留对原始对象的引用”这一点,更准确地说,克隆会对对象的属性进行浅层复制。如果对象包含其他对象,则这些嵌套对象现在将有两个引用。有时这正是所需的,有时则会出现问题。为解决这个问题,对象类需要实现__clone(),就像Stanislav的答案中所示,并根据需要正确处理每个字段。 - ToolmakerSteve
显示剩余4条评论

123

这些问题通常可以在Java书籍中找到答案。

  1. 克隆:

    如果您不重写克隆方法,它的默认行为是浅拷贝。如果您的对象只有原始成员变量,那就没问题了。但是,在另一个对象作为成员变量的无类型语言中,这将是个头痛的问题。

  2. 序列化/反序列化

$new_object = unserialize(serialize($your_object))

这会导致深度拷贝,但其代价取决于对象的复杂性。


4
在PHP中进行深层复制的一个非常好、容易操作的方法,值得大力推荐。但我想问一下关于PHP clone关键字提供的标准浅层复制的问题,你说只有基本成员变量会被复制:那么在PHP中,数组/字符串是否被认为是基本成员变量,因此也会被复制呢? - Marco Demaio
4
针对任何阅读这篇文章的人:进行"浅复制"($a = clone $b,没有使用__clone()魔术方法)等同于按顺序查看对象$b的每个属性,并将其分配给同类的新成员,使用=。作为对象的属性将不会被复制,数组中的对象也是如此;绑定引用的变量同样如此;其他一切都只是值,就像任何赋值一样被复制。 - IMSoP
3
完美!json_decode(json_encode($obj));不会复制私有/受保护的属性和任何方法... unserialize(serialize)也不会复制方法... - zloctb
太棒了!我终于解决了 PhpStorm 的错误;Call to method __clone from invalid context :) - numediaweb
2
这会增加很多运行时开销。您正在将对象序列化为字符串,然后将该字符串解析回新变量中。虽然这确实可以实现您想要的功能,但速度非常慢。您不仅将所有对象转换为字符串并返回,使用时间和内存,还破坏了PHP可能的CopyOnWrite机制。更好的方法是根据下面的https://dev59.com/53VC5IYBdhLWcg3wykQt#186191建议正确实现您的`__clone`方法。有关详细说明,请参见http://www.phpinternalsbook.com/php5/zvals/memory_management.html - Holger Böhnke
显示剩余2条评论

29

根据之前的评论,如果您有另一个对象作为成员变量,请执行以下操作:

class MyClass {
  private $someObject;

  public function __construct() {
    $this->someObject = new SomeClass();
  }

  public function __clone() {
    $this->someObject = clone $this->someObject;
  }

}

现在你可以进行克隆操作:

$bar = new MyClass();
$foo = clone $bar;

12

1
根据文档,它只克隆非对象属性,对于对象属性,它只会通过引用进行复制。这意味着它不是深度复制。 - Bruno Guignard

5

为了澄清,PHP使用写时复制,基本上一切都是引用,直到您对其进行修改,但对于对象,您需要使用克隆和 __clone() 魔术方法,就像在被接受的答案中所述。


1
在这个例子中,我们将创建一个名为 iPhone 的类,并通过克隆来制作完全相同的副本。
class iPhone
{
    public $name;
    public $email;

    public function __construct($n, $e) {

       $this->name = $n;
       $this->email = $e;

    }
}

$main = new iPhone('Dark', 'm@m.com');
$copy = clone $main;

// if you want to print both objects, just write this    

echo "<pre>"; print_r($main);  echo "</pre>";
echo "<pre>"; print_r($copy);  echo "</pre>";

1
这段代码可以帮助克隆方法。
class Foo{

    private $run=10;
    public $foo=array(2,array(2,8));
    public function hoo(){return 5;}


    public function __clone(){

        $this->boo=function(){$this->hoo();};

    }
}
$obj=new Foo;

$news=  clone $obj;
var_dump($news->hoo());

这段代码有点无用,即使你删除 __clone 方法也能正常工作 :) - amik

1

I was doing some testing and got this:

class A {
  public $property;
}

function set_property($obj) {
  $obj->property = "after";
  var_dump($obj);
}

$a = new A();
$a->property = "before";

// Creates a new Object from $a. Like "new A();"
$b = new $a;
// Makes a Copy of var $a, not referenced.
$c = clone $a;

set_property($a);
// object(A)#1 (1) { ["property"]=> string(5) "after" }

var_dump($a); // Because function set_property get by reference
// object(A)#1 (1) { ["property"]=> string(5) "after" }
var_dump($b);
// object(A)#2 (1) { ["property"]=> NULL }
var_dump($c);
// object(A)#3 (1) { ["property"]=> string(6) "before" }

// Now creates a new obj A and passes to the function by clone (will copied)
$d = new A();
$d->property = "before";

set_property(clone $d); // A new variable was created from $d, and not made a reference
// object(A)#5 (1) { ["property"]=> string(5) "after" }

var_dump($d);
// object(A)#4 (1) { ["property"]=> string(6) "before" }

?>


-2
如果您想在不同的实例中完全复制对象的属性,您可以使用以下技术:
将其序列化为 JSON,然后将其反序列化回对象。

10
我会尽量避免它,就像躲避地狱一样。 - Jimmy Kane

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接