객체 복사의 유래
객체에 '복사'라는 개념이 있는 이유는 무엇입니까? 이는 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;
이 코드는 TV 클래스 Television을 정의하고 $tv1은 TV의 인스턴스이며 일반적인 변수 할당 방법을 따릅니다. $tv1의 값은 $t2에 할당됩니다. 이제 $tv1과 $tv2라는 두 대의 TV가 생겼습니다. 이것이 정말 사실인가요? 테스트해 보겠습니다.
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
먼저 tv1과 tv2의 색상이 검은색임을 확인하고 이제 tv2의 색상을 변경하려고 합니다. , 그래서 색상을 흰색으로 설정했습니다. tv2의 색상을 다시 살펴보겠습니다. 실제로는 흰색이 되었는데, 이는 우리의 요구 사항을 충족하는 것 같지만, tv1의 색상을 보면, 우리는 tv1도 검은색 면에서 검은색 면으로 변경된 것을 발견했습니다.
하얀색. tv1의 색상을 재설정하지 않았습니다. tv1이 검정색을 흰색으로 변경한 이유는 무엇입니까? 이는 PHP5에서 객체의 할당과 값 전달이 모두 "참조"에 의해 이루어지기 때문입니다.
PHP5는 Zend Engine II를 사용하며 객체는 독립적인 구조에 저장됩니다.
다른 일반 변수처럼 Zval에 저장되는 대신 Store(PHP4에서는 일반 변수처럼 객체가 Zval에 저장됩니다). Zval에는 객체의 포인터만 저장하고 내용은 저장하지 않습니다.
(값). 객체를 복사하거나 객체를 매개변수로 함수에 전달할 때 데이터를 복사할 필요가 없습니다. 동일한 객체 포인터를 유지하고 이 특정 객체가 이제 다른 zval에 의해 가리키는 객체에 알립니다.
가게. 객체 자체가 Object에 위치하므로
Store, 모든 변경 사항은 객체에 대한 포인터를 보유하는 모든 zval 구조에 영향을 미칩니다. 프로그램에 반영되며 대상 객체에 대한 모든 변경 사항은 소스 객체에 영향을 미칩니다. .이것은
PHP 객체는 항상 참조로 전달되는 것으로 보입니다. 따라서 위의 tv2와 tv1은 실제로 동일한 TV 인스턴스를 가리키며 tv1 또는 tv2에서 수행하는 작업은 실제로 이 동일한 인스턴스에 대한 것입니다. 그래서 우리의 "복사"는 실패했습니다. 이 때문에 직접 변수 할당 방식으로는 객체를 복사할 수 없는 것 같습니다.
PHP5는 객체 복사를 위해 특별히 복제 작업을 제공합니다. 여기서 객체 복사가 시작됩니다.
복제본을 사용하여 객체 복사
이제 PHP5의 복제 언어 구조를 사용하여 객체를 복사합니다. 코드는 다음과 같습니다.
[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
이 코드의 두 번째 줄에서는 tv1을 복사하기 위해 clone 키워드를 사용합니다. 이제 tv1과 tv2의 실제 복사본이 있는지 여부를 감지합니다. 성공하든 아니든요. tv2의 색상을 흰색으로 변경했고, tv1의 색상은 여전히 검은색이므로 복사 작업이 성공한 것을 볼 수 있습니다.
__clone 매직 메소드
이제 각 TV마다 고유한 번호가 있어야 하는 상황을 고려합니다. 이 번호는 ID 번호와 마찬가지로 고유해야 하므로 TV를 복사할 때 문제가 발생하지 않도록 번호도 복사되었습니다. 우리가 생각해낸 전략 중 하나는 할당된 TV 번호를 지운 다음 필요에 따라 번호를 다시 할당하는 것입니다.
그런 다음 이러한 문제를 해결하기 위해 특별히 __clone 매직 메서드가 사용됩니다. 객체가 복사될 때(즉, 복제 작업) __clone 매직 메서드가 트리거됩니다. TV 클래스 Television의 코드를 수정하고 number 속성과 __clone 메서드를 추가했습니다.
PHP 코드
* /** * * 电视机类 * */ * 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); * } * }
이런 TV 객체를 복사해 보겠습니다.
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
TV1을 제작했습니다.
그 번호를 111111로 설정한 다음 tv1을 복사하여 tv2를 가져옵니다. 이때 __clone 매직 메서드가 트리거됩니다. 우리는 __clone 메서드에서 setIdentity 멤버를 호출했습니다. 이 메서드는 나중에 번호를 다시 매길 수 있도록 tv2의 _identity 속성을 지웁니다. 이것으로부터 우리는 __clone 매직 메소드를 사용하면 객체를 복제할 때 몇 가지 추가 작업을 매우 편리하게 수행할 수 있다는 것을 알 수 있습니다.
클론 작업의 치명적인 결함
클론이 과연 이상적인 복사 효과를 얻을 수 있을까? 어떤 경우에는 복제 작업이 우리가 상상했던 것만큼 완벽하지 않다는 것을 알게 될 것입니다. 위의 TV 유형을 수정한 후 테스트해 보겠습니다.
각 TV에는 리모컨이 함께 제공되므로 리모컨 수업을 진행합니다. 리모컨과 TV는 '집합' 관계입니다('결합' 관계에 비해 약한 종속 관계이기 때문). 일반적으로 TV는 리모콘 없이도 정상적으로 사용할 수 있습니다. 이제 TV 개체는 모두 리모콘 개체에 대한 참조를 보유해야 합니다. 아래 코드를 보세요
PHP 코드
* /** * * 电视机类 * */ * 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 * { * * }
아래의 그러한 TV 개체를 복사하여 TV의 리모컨 개체를 관찰해 보세요.
PHP 코드
* $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中借鉴很多宝贵的经验。