首頁  >  文章  >  php教程  >  php5物件複製、clone、淺複製與深複製

php5物件複製、clone、淺複製與深複製

大家讲道理
大家讲道理原創
2016-11-08 14:36:311309瀏覽

物件複製的由來 
為什麼物件會有「複製」這個概念,這與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 
      &#39;<br>&#39;;  
    * echo 
      &#39;color of tv2 is: &#39; . $tv2->getColor();//tv2的颜色是black 
    * echo 
      &#39;<br>&#39;;  
    *   
    * //把tv2涂成白色 
    * $tv2->setColor(&#39;white&#39;);  
    *   
    * echo 
      &#39;color of tv2 is: &#39; . $tv2->getColor();//tv2的颜色是white 
    * echo 
      &#39;<br>&#39;;  
    * echo 
      &#39;color of tv1 is: &#39; . $tv1->getColor();//tv1的颜色是white


首先我們看到tv1和tv2的顏色都是black,現在我們希望tv2tv2的顏色,所以我們把它的顏色設定成了white,我們再看看確實成為了white,似乎滿足了我們的要求,可是並沒有想像中的那麼順利,當我們接著看tv1的顏色的時候,我們發現tv1也由black邊成了 white。我們並沒有重新設定tv1的顏色,為什麼tv1會重black變成white呢?這是因為PHP5中物件的賦值和傳值都是以「引用」的方式。 PHP5 使用了Zend引擎II,物件被儲存於獨立的結構Object Store中,而不像其它一般變數那樣儲存於Zval中(在PHP4中物件和一般變數一樣儲存於Zval)。在Zval中僅儲存物件的指標而非內容 (value)。當我們複製一個物件或將一個物件當作參數傳遞給一個函數時,我們不需要複製資料。僅保持相同的物件指標並由另一個zval通知現在這個特定的物件所指向的Object Store。由於物件本身位於Object Store,我們對它所做的任何改變將影響到所有持有該物件指標的zval結構----表現在程式中就是目標物件的任何改變都會影響到來源物件。 。這使 PHP物件看起來就像總是透過引用(reference)來傳遞。所以以上的tv2和tv1其實是指向同一個電視機實例,我們對tv1或則tv2所做的操作其實都是針對這同一個實例。因此我們的「複製」失敗了。看來直接變數賦值的方式並不能拷貝對象,為此 PHP5提供了一個專門用來複製物件的操作,也就是 clone 。這就是物件複製的由來。


用clone(克隆)來複製對象 

我們現在使用PHP5的clone語言結構來複製對象,代碼如下: 

[size=+0]PHP代碼 

    * [size=+0]$tv1 = new Television();  
    * $tv2 = clone $tv1;  
    * 
    * echo 
      &#39;color of tv1 is: &#39; . $tv1->getColor();//tv1的颜色是black 
    * echo 
      &#39;<br>&#39;;  
    * echo 
      &#39;color of tv2 is: &#39; . $tv2->getColor();//tv2的颜色是black 
    * echo 
      &#39;<br>&#39;;  
    * 
    * //把tv2换成涂成白色 
    * $tv2->setColor(&#39;white&#39;);  
    * 
    * echo 
      &#39;color of tv2 is: &#39; . $tv2->getColor();//tv2的颜色是white 
    * echo 
      &#39;<br>&#39;;  
    * echo 
      &#39;color of tv1 is: &#39; . $tv1->getColor();//tv1的颜色是black


用clone關鍵字複製tv1,現在我們就擁有了一份真正的tv1的拷貝tv2,我們還是按照之前的方法來檢測複製是否成功。我們可以看到,我們將tv2的顏色換成了white,tv1的顏色還是black,這樣我們的複製操作就成功了。 



__clone魔術方法 

    現在我們考慮到這樣一個情況,每一台電視機應該都有自己的編號,這個編號如同我們的身份證號碼一樣應該是唯一的,所以當我們在複製一台電視機的時候,我們不希望這個編號也被複製過來,以免造成一些麻煩。我們想到的一個策略是將賦值出來的電視機的編號清空,然後再依照需求重新指派編號。 
    那麼__clone魔術方法就是專門用來解決這樣的問題,__clone魔術方法會在對像被複製( 也就是clone操作)的時候被觸發。我們修改了電視機類別Television的程式碼,加入了編號屬性和__clone方法,程式碼如下。 

PHP代碼 



    * /** 
    * * 电视机类 
    * */ 
    * class Television   
    * {  
    *       
    *     /** 
    *      * 电视机编号 
    *      */ 
    *     protected 
      $_identity    = 0;  
    *       
    *     /** 
    *      * 屏幕高度 
    *      */ 
    *     protected 
      $_screenLength = 300;  
    *       
    *     /** 
    *      * 屏幕宽度 
    *      */ 
    *     protected 
      $_screenHight  = 200;  
    *       
    *     /** 
    *      * 电视机外观颜色 
    *      */ 
    *     protected 
      $_color        = &#39;black&#39;;  
    *       
    *     /** 
    *      * 返回电视外观颜色 
    *      */ 
    *     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(&#39;111111&#39;);  
    * echo 
      &#39;id of tv1 is &#39; . $tv1->getIdentity();//111111  
    * echo 
      &#39;<br>&#39;;  
    *   
    * $tv2 = clone $tv1;  
    * echo 
      &#39;id of tv2 is &#39; . $tv2->getIdentity();//0


我們生產了一台電視機tv1 , 並且設定它的編號為111111,然後我們用clone將tv1複製得到了tv2,這個時候__clone魔術方法被觸發,此方法將直接作用與複製得到的對象tv2,我們在__clone方法中調用了setIdentity成員方法將tv2的_identity屬性清空,以便我們後面對它進行重新編號。由此我們可以看到__clone魔術方法能讓我們非常方便的在clone物件的時候做一些附加的操作。 

clone操作的致命缺陷 
    clone真的能夠達到理想的複製效果嗎?在某些情況下,你應該會發現,clone操作並沒有我們想像中的完美。我們將以上的電視機類別修改一下,然後做測試。
    每台電視機都會附帶一個遙控器,所以我們將會有一個遙控器類,遙控器和電視機是一種「聚合」關係(相對與「組合」關係,是一種較弱的依賴關係,因為一般情況電視機就算沒有遙控也能正常使用),現在我們的電視機物件應該都持有一個到遙控器物件的引用。下面看看代碼 

PHP代碼 



    * /** 
    * * 电视机类 
    * */ 
    * class Television   
    * {  
    *       
    *     /** 
    *      * 电视机编号 
    *      */ 
    *     protected 
      $_identity    = 0;  
    *       
    *     /** 
    *      * 屏幕高度 
    *      */ 
    *     protected 
      $_screenLength = 300;  
    *       
    *     /** 
    *      * 屏幕宽度 
    *      */ 
    *     protected 
      $_screenHight  = 200;  
    *       
    *     /** 
    *      * 电视机外观颜色 
    *      */ 
    *     protected 
      $_color        = &#39;black&#39;;  
    *       
    *     /** 
    *      * 遥控器对象 
    *      */ 
    *     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   
    * {  
    *   
    * }


下面複製這樣的一個電視機對象並且觀察電視機的遙控器對象。 

PHP代碼 🎜🎜🎜
    * $tv1 = new Television();  
    * $tv2 = clone $tv1;  
    *   
    * $contr1 = $tv1->getControl(); //获取tv1的遥控器contr1 
    * $contr2 = $tv2->getControl(); //获取tv2的遥控器contr2 
    * echo 
      $tv1;    //tv1的object id 为 #1 
    * echo 
      &#39;<br>&#39;;  
    * echo 
      $contr1; //contr1的object id 为#2 
    * echo 
      &#39;<br>&#39;;   
    * echo 
      $tv2;    //tv2的object id 为 #3 
    * echo 
      &#39;<br>&#39;;  
    * 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 
      &#39;<br>&#39;;  
    * echo 
      $contr1; //contr1的object id 为#2 
    * echo 
      &#39;<br>&#39;;   
    * echo 
      $tv2;    //tv2的object id 为 #4 
    * echo 
      &#39;<br>&#39;;  
    * echo 
      $contr2; //contr2的object id 为#5

我们可以看到输出结果,tv1和tv2拥有了不同的遥控器。这比方案1要方便很多,序列化是一个递归的过程,我们不需要理会被对象内部引用了多少个对象以及引用了多少层对象,我们都可以彻底的复制。注意使用此方案时我们无法触发__clone魔术方法来完成一些附加操作,当然我们可以在深复制之后再进行一次clone操作来触发__clone魔术方法,只是会对效率些小的影响。另外此方案会触发被复制对象和所有被引用对象的__sleep和__wakeup魔术方法,所以这些情况都需要被考虑。 



总结 
   不同的对象复制方式有着不同的效果,我们应该根据具体应用需求来考虑使用哪种方式以及如何改进复制方式。PHP5的面向对象特性和JAVA比较接近,相信我们可以从JAVA中借鉴很多宝贵的经验。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn