我覺得有必要說明一下,__clone 並非覆寫(override)。如同範例所示,一般的複製流程仍然會執行,__clone 方法的職責是「修正」一般複製流程中任何「錯誤」的動作。
建立一個物件的副本並完全複製其屬性並非總是我們想要的行為。一個需要複製建構函式的好例子是:如果您有一個代表 GTK 視窗的物件,且該物件持有此 GTK 視窗的資源,當您建立副本時,您可能想要建立一個具有相同屬性的新視窗,並讓新的物件持有新視窗的資源。另一個例子是:如果您的物件持有它所使用的另一個物件的參考,當您複製父物件時,您想要建立這個另一個物件的新實例,以便副本擁有其獨立的副本。
物件副本是使用 `clone` 關鍵字建立的(如果可能的話,它會呼叫物件的 __clone() 方法)。
$copy_of_object = clone $object;
當一個物件被複製時,PHP 會對該物件的所有屬性執行淺層複製 (shallow copy)。任何作為其他變數參考的屬性都將保持為參考。
複製完成後,如果定義了 __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 並非覆寫(override)。如同範例所示,一般的複製流程仍然會執行,__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。
類別 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
不言而喻,如果你有循環參考,其中物件 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);
這個基底類別會自動遞迴地複製物件類型的屬性或物件類型陣列值。只需讓您自己的類別繼承自這個基底類別。
<?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";
?>
可以知道一個物件被複製了多少次。我認為這是正確的
<?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("原始物件:\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] =>
)
)