2024 年日本 PHP 研討會

物件複製

建立一個物件的副本並完全複製其屬性並非總是我們想要的行為。一個需要複製建構函式的好例子是:如果您有一個代表 GTK 視窗的物件,且該物件持有此 GTK 視窗的資源,當您建立副本時,您可能想要建立一個具有相同屬性的新視窗,並讓新的物件持有新視窗的資源。另一個例子是:如果您的物件持有它所使用的另一個物件的參考,當您複製父物件時,您想要建立這個另一個物件的新實例,以便副本擁有其獨立的副本。

物件副本是使用 `clone` 關鍵字建立的(如果可能的話,它會呼叫物件的 __clone() 方法)。

$copy_of_object = clone $object;

當一個物件被複製時,PHP 會對該物件的所有屬性執行淺層複製 (shallow copy)。任何作為其他變數參考的屬性都將保持為參考。

__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 並非覆寫(override)。如同範例所示,一般的複製流程仍然會執行,__clone 方法的職責是「修正」一般複製流程中任何「錯誤」的動作。
jojor at gmx dot net
14 年前
這是我的測試腳本,用來測試當類別中包含含有基本型別值的陣列時,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>
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

類別 Foo
{
public $that;

函式
__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
類別 clone_base
{
公開 函式 __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
'Clones:' . Classe::howManyClones() . PHP_EOL;

unset(
$d);

echo
'Clones:' . 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 方法)。

為了說明這個過程,以下的程式碼範例似乎更好,與原始版本進行比較

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("原始物件:\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