物件複製的由來
為什麼物件會有「複製」這個概念,這與PHP5中物件的傳值方式是密切相關的,讓我們看看下面這段簡單的程式碼
PHP程式碼
* /** * * 电视机类 * */ * class Television * { * /** * * 屏幕高度 * */ * protected $_screenLength = 300; * * /** * * 屏幕宽度 * */ * protected $_screenHight = 200; * * /** * * 电视机外观颜色 * */ * protected $_color = 'black'; * * /** * * 返回电视外观颜色 * */ * public function getColor() * { * return $this->_color; * } * * /** * * 设置电视机外观颜色 * */ * public function setColor($color) * { * $this->_color = (string)$color; * return $this; * } * } * * $tv1 = new Television(); * $tv2 = $tv1;
PHP程式碼
這段程式碼定義了一個電視機的類別Television , $tv1為一個電視機的實例,然後我們按照普通的變數賦值方式將$tv1的值賦給$t2。那麼現在我們擁有兩台電視機$tv1和$tv2了,真的是這樣的嗎?我們來測試一下。
PHP代碼
* echo 'color of tv1 is: ' . $tv1->getColor();//tv1的颜色是black * echo '<br>'; * echo 'color of tv2 is: ' . $tv2->getColor();//tv2的颜色是black * echo '<br>'; * * //把tv2涂成白色 * $tv2->setColor('white'); * * echo 'color of tv2 is: ' . $tv2->getColor();//tv2的颜色是white * echo '<br>'; * echo 'color of tv1 is: ' . $tv1->getColor();//tv1的颜色是white
[size=+0]PHP代碼
* [size=+0]$tv1 = new Television(); * $tv2 = clone $tv1; * * echo 'color of tv1 is: ' . $tv1->getColor();//tv1的颜色是black * echo '<br>'; * echo 'color of tv2 is: ' . $tv2->getColor();//tv2的颜色是black * echo '<br>'; * * //把tv2换成涂成白色 * $tv2->setColor('white'); * * echo 'color of tv2 is: ' . $tv2->getColor();//tv2的颜色是white * echo '<br>'; * echo 'color of tv1 is: ' . $tv1->getColor();//tv1的颜色是black
* /** * * 电视机类 * */ * class Television * { * * /** * * 电视机编号 * */ * protected $_identity = 0; * * /** * * 屏幕高度 * */ * protected $_screenLength = 300; * * /** * * 屏幕宽度 * */ * protected $_screenHight = 200; * * /** * * 电视机外观颜色 * */ * protected $_color = 'black'; * * /** * * 返回电视外观颜色 * */ * public function getColor() * { * return $this->_color; * } * * /** * * 设置电视机外观颜色 * */ * public function setColor($color) * { * $this->_color = (string)$color; * return $this; * } * * /** * * 返回电视机编号 * */ * public function getIdentity() * { * return $this->_identity; * } * * /** * * 设置电视机编号 * */ * public function setIdentity($id) * { * $this->_identity = (int)$id; * return $this; * } * * public function __clone() * { * $this->setIdentity(0); * } * }
PHP代碼
* $tv1 = new Television(); * $tv1->setIdentity('111111'); * echo 'id of tv1 is ' . $tv1->getIdentity();//111111 * echo '<br>'; * * $tv2 = clone $tv1; * echo 'id of tv2 is ' . $tv2->getIdentity();//0
* /** * * 电视机类 * */ * class Television * { * * /** * * 电视机编号 * */ * protected $_identity = 0; * * /** * * 屏幕高度 * */ * protected $_screenLength = 300; * * /** * * 屏幕宽度 * */ * protected $_screenHight = 200; * * /** * * 电视机外观颜色 * */ * protected $_color = 'black'; * * /** * * 遥控器对象 * */ * protected $_control = null; * * /** * * 构造函数中加载遥控器对象 * */ * public function __construct() * { * $this->setControl(new Telecontrol()); * } * * /** * * 设置遥控器对象 * */ * public function setControl(Telecontrol $control) * { * $this->_control = $control; * return $this; * } * * /** * * 返回遥控器对象 * */ * public function getControl() * { * return $this->_control; * } * * /** * * 返回电视外观颜色 * */ * public function getColor() * { * return $this->_color; * } * * /** * * 设置电视机外观颜色 * */ * public function setColor($color) * { * $this->_color = (string)$color; * return $this; * } * * /** * * 返回电视机编号 * */ * public function getIdentity() * { * return $this->_identity; * } * * /** * * 设置电视机编号 * */ * public function setIdentity($id) * { * $this->_identity = (int)$id; * return $this; * } * * public function __clone() * { * $this->setIdentity(0); * } * } * * * /** * * 遥控器类 * */ * class Telecontrol * { * * }
* $tv1 = new Television(); * $tv2 = clone $tv1; * * $contr1 = $tv1->getControl(); //获取tv1的遥控器contr1 * $contr2 = $tv2->getControl(); //获取tv2的遥控器contr2 * echo $tv1; //tv1的object id 为 #1 * echo '<br>'; * echo $contr1; //contr1的object id 为#2 * echo '<br>'; * echo $tv2; //tv2的object id 为 #3 * echo '<br>'; * echo $contr2; //contr2的object id 为#2
经过复制之后,我们查看对象id,通过clone操作从tv1复制出了tv2,tv1和tv2的对象id分别是
1和3,这表示tv1和tv2是引用两个不同的电视机对象,这符合clone操作的结果。然后我们分别获取了tv1的遥控器对象contr1和tv2的遥控器对象contr2,通过查看它们的对象
id我们发现contr1和contr2的对象id都是2,这表明它们是到同一个对象的引用,也就是说我们虽然从tv1复制出tv2,但是遥控器并没有被复制,每台电视机都应该配有一个遥控器,而这里tv2和tv1共用一个遥控器,这显然是不合常理的。
由此可见,clone操作有这么一个非常大的缺陷:使用clone操作复制对象时,当被复制的对象有对其它对象的引用的时候,引用的对象将不会被复制。然而这种情况又非常的普遍,现今
“合成/聚合复用”多被提倡用来代替“继承复用”,“合成”和“聚合”就是让一个对象拥有对另一个对象的引用,从而复用被引用对象的方法。我们在使用
clone的时候应该考虑到这样的情况。那么在clone对象的时候我们应该如何去解决这样的一个缺陷呢?可能你很快就想到了之前提到的__clone魔术方法,这确实是一种解决方案。
方案1:用__clone魔术方法弥补
前面我们已经介绍了__clone魔术方法的用法,我们可以在__clone方法中将被复制对象中其它对象的引用重新引用到一个新的对象。下面我们看看修改后的__clone()魔术方法:
[size=+0][size=+0]PHP代码
* [size=+0][size=+0]public function __clone() * { * $this->setIdentity(0); * //重新设置一个遥控器对象 * $this->setControl(new Telecontrol()); * }
第04行中我们为复制出来的电视机对象重新设置了一个遥控器,我们按照之前的方法查看对象的id可以发现,两台电视机的遥控器拥有不同的对象id,这样我们的问题就解决了。
但是这样的方式大概并不算太好,如果被复制对象中有多个到其它对象的引用,我们必须在__clone方法中逐个的重新设置,更糟糕的是如果被复制对象的类由第三方提供,我们无法修改代码,那复制操作基本就无法顺利完成了。
我们使用clone来复制对象,这种复制叫做“浅复制”:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。也就是说,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。相对于“浅复制”,当然也有一个“深复制”:被复制的对象的所有的变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。也就是说,深复制把要复制的对象所引用的对象都复制了一遍。深复制需要决定深入到多少层,这是一个不容易确定的问题,此外可能会出现循环引用的问题,这些都必须小心处理。我们的方案2将是一个深复制的解决方案。
方案2:利用串行化做深复制
PHP有串行化(serialize)和反串行化(unserialize)函数,我们只需要用serialize()将一个对象写入一个流,然后从流中读回对象,那么对象就被复制了。在JAVA语言里面,这个过程叫做“冷藏”和“解冻”。下面我们将测试一下这个方法:
[size=+0][size=+0]PHP代码
* [size=+0][size=+0]$tv1 = new Television(); * $tv2 = unserialize(serialize($tv1));//序列化然后反序列化 * * $contr1 = $tv1->getControl(); //获取tv1的遥控器contr1 * $contr2 = $tv2->getControl(); //获取tv2的遥控器contr2 * * echo $tv1; //tv1的object id 为 #1 * echo '<br>'; * echo $contr1; //contr1的object id 为#2 * echo '<br>'; * echo $tv2; //tv2的object id 为 #4 * echo '<br>'; * echo $contr2; //contr2的object id 为#5
我们可以看到输出结果,tv1和tv2拥有了不同的遥控器。这比方案1要方便很多,序列化是一个递归的过程,我们不需要理会被对象内部引用了多少个对象以及引用了多少层对象,我们都可以彻底的复制。注意使用此方案时我们无法触发__clone魔术方法来完成一些附加操作,当然我们可以在深复制之后再进行一次clone操作来触发__clone魔术方法,只是会对效率些小的影响。另外此方案会触发被复制对象和所有被引用对象的__sleep和__wakeup魔术方法,所以这些情况都需要被考虑。
总结
不同的对象复制方式有着不同的效果,我们应该根据具体应用需求来考虑使用哪种方式以及如何改进复制方式。PHP5的面向对象特性和JAVA比较接近,相信我们可以从JAVA中借鉴很多宝贵的经验。