1、对象复制的由来

  为什么对象会有“复制”这个概念,这与PHP5中对象的传值方式是密切相关的,让我们看看下面这段简单的代码

  1. /**
  2. * 电视机类
  3. */
  4. class Television
  5. {
  6. /**
  7. * 屏幕高度
  8. */
  9. protected $_screenLength = 300;
  10.  
  11. /**
  12. * 屏幕宽度
  13. */
  14. protected $_screenHight = 200;
  15.  
  16. /**
  17. * 电视机外观颜色
  18. */
  19. protected $_color = 'black';
  20.  
  21. /**
  22. * 返回电视外观颜色
  23. */
  24. public function getColor()
  25. {
  26. return $this->_color;
  27. }
  28.  
  29. /**
  30. * 设置电视机外观颜色
  31. */
  32. public function setColor($color)
  33. {
  34. $this->_color = (string)$color;
  35. return $this;
  36. }
  37. }
  38.  
  39. $tv1 = new Television();
  40. $tv2 = $tv1;

这段代码定义了一个电视机的类 Television , $tv1为一个电视机的实例,然后我们按照普通的变量赋值方式将$tv1的值赋给$t2。那么现在我们拥有两台电视机$tv1和$tv2了,真的是这样的吗?我们来测试一下。

  1. echo 'color of tv1 is: ' . $tv1->getColor();//tv1的颜色是black
  2. echo '<br>';
  3. echo 'color of tv2 is: ' . $tv2->getColor();//tv2的颜色是black
  4. echo '<br>';
  5.  
  6. //把tv2涂成白色
  7. $tv2->setColor('white');
  8.  
  9. echo 'color of tv2 is: ' . $tv2->getColor();//tv2的颜色是white
  10. echo '<br>';
  11. echo 'color of tv1 is: ' . $tv1->getColor();//tv1的颜色是white

  首先我们看到tv1和tv2的颜色都是black,现在我们希望tv2换个颜色,所以我们将它的颜色设置成了white,我们再看看tv2的颜色,确实成为了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 。这就是对象复制的由来

2、用clone(克隆)来复制对象

我们现在使用PHP5的clone语言结构来复制对象,代码如下:

  1. $tv1 = new Television();
  2. $tv2 = clone $tv1;
  3.  
  4. echo 'color of tv1 is: ' . $tv1->getColor();//tv1的颜色是black
  5. echo '<br>';
  6. echo 'color of tv2 is: ' . $tv2->getColor();//tv2的颜色是black
  7. echo '<br>';
  8.  
  9. //把tv2换成涂成白色
  10. $tv2->setColor('white');
  11.  
  12. echo 'color of tv2 is: ' . $tv2->getColor();//tv2的颜色是white
  13. echo '<br>';
  14. echo 'color of tv1 is: ' . $tv1->getColor();//tv1的颜色是black

这段代码的第2行,我们用clone关键字复制tv1,现在我们就拥有了一份真正的tv1的拷贝tv2,我们还是按照之前的方法来检测复制是否成功。我们可以看到,我们将tv2的颜色换成了white,tv1的颜色还是black,这样我们的复制操作就成功了。

3、__clone魔术方法

  现在我们考虑到这样一个情况,每一台电视机应该都有自己的编号,这个编号如同我们的身份证号码一样应该是唯一的,所以当我们在复制一台电视机的时候,我们不希望这个编号也被复制过来,以免造成一些麻烦。我们想到的一个策略是将赋值出来的电视机的编号清空,然后再按照需求来重新分配编号。

那么__clone魔术方法就是专门用来解决这样的问题,__clone魔术方法会在对象被复制( 也就是clone操作)的时候被触发。我们修改了电视机类Television的代码,添加了编号属性和__clone方法,代码如下。

  1. /**
  2. * 电视机类
  3. */
  4. class Television
  5. {
  6.  
  7. /**
  8. * 电视机编号
  9. */
  10. protected $_identity = 0;
  11.  
  12. /**
  13. * 屏幕高度
  14. */
  15. protected $_screenLength = 300;
  16.  
  17. /**
  18. * 屏幕宽度
  19. */
  20. protected $_screenHight = 200;
  21.  
  22. /**
  23. * 电视机外观颜色
  24. */
  25. protected $_color = 'black';
  26.  
  27. /**
  28. * 返回电视外观颜色
  29. */
  30. public function getColor()
  31. {
  32. return $this->_color;
  33. }
  34.  
  35. /**
  36. * 设置电视机外观颜色
  37. */
  38. public function setColor($color)
  39. {
  40. $this->_color = (string)$color;
  41. return $this;
  42. }
  43.  
  44. /**
  45. * 返回电视机编号
  46. */
  47. public function getIdentity()
  48. {
  49. return $this->_identity;
  50. }
  51.  
  52. /**
  53. * 设置电视机编号
  54. */
  55. public function setIdentity($id)
  56. {
  57. $this->_identity = (int)$id;
  58. return $this;
  59. }
  60.  
  61. public function __clone()
  62. {
  63. $this->setIdentity(0);
  64. }
  65. }

下面我们来复制这样的一个电视机对象

  1. $tv1 = new Television();
  2. $tv1->setIdentity('111111');
  3. echo 'id of tv1 is ' . $tv1->getIdentity();//111111
  4. echo '<br>';
  5.  
  6. $tv2 = clone $tv1;
  7. echo 'id of tv2 is ' . $tv2->getIdentity();//0

我们生产了一台电视机tv1 , 并且设置它的编号为111111,然后我们用clone将tv1复制得到了tv2,这个时候__clone魔术方法被触发,此方法将直接作用与复制得到的对象tv2,我们在__clone方法中调用了setIdentity成员方法将tv2的_identity属性清空,以便我们后面对它进行重新编号。由此我们可以看到__clone魔术方法能让我们非常方便的在clone对象的时候做一些附加的操作。

4、clone操作的致命缺陷

  clone真的能够达到理想的复制效果吗?在某些情况下,你应该会发现,clone操作并没有我们想象中的那么完美。我们将以上的电视机类修改一下,然后做测试。

每台电视机都会附带一个遥控器,所以我们将会有一个遥控器类,遥控器和电视机是一种“聚合”关系(相对与“组合”关系,是一种较弱的依赖关系,因为一般情况电视机就算没有遥控也能正常使用),现在我们的电视机对象应该都持有一个到遥控器对象的引用。下面看看代码

  1. /**
  2. * 电视机类
  3. */
  4. class Television
  5. {
  6.  
  7. /**
  8. * 电视机编号
  9. */
  10. protected $_identity = 0;
  11.  
  12. /**
  13. * 屏幕高度
  14. */
  15. protected $_screenLength = 300;
  16.  
  17. /**
  18. * 屏幕宽度
  19. */
  20. protected $_screenHight = 200;
  21.  
  22. /**
  23. * 电视机外观颜色
  24. */
  25. protected $_color = 'black';
  26.  
  27. /**
  28. * 遥控器对象
  29. */
  30. protected $_control = null;
  31.  
  32. /**
  33. * 构造函数中加载遥控器对象
  34. */
  35. public function __construct()
  36. {
  37. $this->setControl(new Telecontrol());
  38. }
  39.  
  40. /**
  41. * 设置遥控器对象
  42. */
  43. public function setControl(Telecontrol $control)
  44. {
  45. $this->_control = $control;
  46. return $this;
  47. }
  48.  
  49. /**
  50. * 返回遥控器对象
  51. */
  52. public function getControl()
  53. {
  54. return $this->_control;
  55. }
  56.  
  57. /**
  58. * 返回电视外观颜色
  59. */
  60. public function getColor()
  61. {
  62. return $this->_color;
  63. }
  64.  
  65. /**
  66. * 设置电视机外观颜色
  67. */
  68. public function setColor($color)
  69. {
  70. $this->_color = (string)$color;
  71. return $this;
  72. }
  73.  
  74. /**
  75. * 返回电视机编号
  76. */
  77. public function getIdentity()
  78. {
  79. return $this->_identity;
  80. }
  81.  
  82. /**
  83. * 设置电视机编号
  84. */
  85. public function setIdentity($id)
  86. {
  87. $this->_identity = (int)$id;
  88. return $this;
  89. }
  90.  
  91. public function __clone()
  92. {
  93. $this->setIdentity(0);
  94. }
  95. }
  96.  
  97. /**
  98. * 遥控器类
  99. */
  100. class Telecontrol
  101. {
  102.  
  103. }

下面复制这样的一个电视机对象并且观察电视机的遥控器对象。

  1. $tv1 = new Television();
  2. $tv2 = clone $tv1;
  3.  
  4. $contr1 = $tv1->getControl(); //获取tv1的遥控器contr1
  5. $contr2 = $tv2->getControl(); //获取tv2的遥控器contr2
  6. echo $tv1; //tv1的object id 为 #1
  7. echo '<br>';
  8. echo $contr1; //contr1的object id 为#2
  9. echo '<br>';
  10. echo $tv2; //tv2的object id 为 #3
  11. echo '<br>';
  12. 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()魔术方法:

  1. public function __clone()
  2. {
  3. $this->setIdentity(0);
  4. //重新设置一个遥控器对象
  5. $this->setControl(new Telecontrol());
  6. }

第04行中我们为复制出来的电视机对象重新设置了一个遥控器,我们按照之前的方法查看对象的id可以发现,两台电视机的遥控器拥有不同的对象id,这样我们的问题就解决了。

但是这样的方式大概并不算太好,如果被复制对象中有多个到其它对象的引用,我们必须在__clone方法中逐个的重新设置,更糟糕的是如果被复制对象的类由第三方提供,我们无法修改代码,那复制操作基本就无法顺利完成了。

我们使用clone来复制对象,这种复制叫做“浅复制”被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。也就是说,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。相对于“浅复制”,当然也有一个“深复制”被复制的对象的所有的变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。也就是说,深复制把要复制的对象所引用的对象都复制了一遍。深复制需要决定深入到多少层,这是一个不容易确定的问题,此外可能会出现循环引用的问题,这些都必须小心处理。我们的方案2将是一个深复制的解决方案。

2)利用串行化做深复制

  PHP有串行化(serialize)和反串行化(unserialize)函数,我们只需要用serialize()将一个对象写入一个流,然后从流中读回对象,那么对象就被复制了。在JAVA语言里面,这个过程叫做“冷藏”和“解冻”。下面我们将测试一下这个方法:

  1. $tv1 = new Television();
  2. $tv2 = unserialize(serialize($tv1));//序列化然后反序列化
  3.  
  4. $contr1 = $tv1->getControl(); //获取tv1的遥控器contr1
  5. $contr2 = $tv2->getControl(); //获取tv2的遥控器contr2
  6.  
  7. echo $tv1; //tv1的object id 为 #1
  8. echo '<br>';
  9. echo $contr1; //contr1的object id 为#2
  10. echo '<br>';
  11. echo $tv2; //tv2的object id 为 #4
  12. echo '<br>';
  13. echo $contr2; //contr2的object id 为#5

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

PHP的深copy和浅copy的更多相关文章

  1. (五)聊一聊深Copy与浅Copy

    一.关于浅copy与深copy 首先说明一下: 在python中,赋值其实就是对象的引用,变量就是对象的一个标签,如果把内存对象比喻成一个个房间,那么变量就是门牌号. 深copy与浅copy只是针对可 ...

  2. Day 7 深copy和浅Copy

    dict.fromkeys的用法 1 2 3 4 5 6 7 8 9 10 11 #dict.fromkeys的用法 #例子1 dic = dict.fromkeys([1,2,3],[]) prin ...

  3. Python的深copy和浅copy

    浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象. 深拷贝(deepcopy): copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象. 浅copy: a = [1, 2, ...

  4. 列表的使用2,深COPY和浅COPY,循环列表,步长切片

    name2=names.copy() 下面我看几种奇怪的现象: 首先把源列表copy以后,把源列表第2个数值修改.那么没毛病. 如果源列表里,还包含了一个子列表:这也没毛病 如果我们这个时候修改子列表 ...

  5. 深copy和浅copy

    浅copy:其实就是将容器中的内存地址存放进另一个容器中,所以两个容器本身的内存地址不相同,但容器里面的内存地址相同 代码如下: 深copy:就是从里到外完完全全复制了所有值,存进另外的内存空间,并赋 ...

  6. python中深copy,浅copy与赋值语句的区别

    以下详细讲解:python深复制,浅复制与赋值语句的区别 1. '='赋值语句,常规复制只是将另一个变量名关联到了列表,并不进行副本复制,实例如下: var1=[12,35,67,91,101]var ...

  7. C/C++深度copy和浅copy

    #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string. ...

  8. 深浅copy和浅copy

    深浅copy 1,先看赋值运算. l1 = [1,2,3,['barry','alex']] l2 = l1 l1[0] = 111 print(l1) # [111, 2, 3, ['barry', ...

  9. python中深copy,浅copy

    版权声明:本文为博主原创文章,未经博主允许不得转载. >>> mylist1 = [1, 2, 3, 4] >>> myl = mylist1 >>&g ...

随机推荐

  1. java:可变参数(转载)

    http://12477787.blog.51cto.com/12467787/1887843 Java在1.5之后允许方法使用可变参数,可变参数的好处在于:它允许传递0个或者多个参数.比如原来有一段 ...

  2. C​P​U​_​C​S​t​a​t​e​_​P​S​t​a​t​e and then ACPI on Wiki

    http://wenku.baidu.com/link?url=eHbdT4EjdJx3dsQETGUIL8q1K3_EyuzGLWT0G103AEca0vs0gHR_v_3c0oaUL2gbkrr8 ...

  3. Redis(九):使用RedisTemplate访问Redis数据结构API大全

    RedisTemplate介绍 spring封装了RedisTemplate对象来进行对redis的各种操作,它支持所有的 redis 原生的api. RedisTemplate在spring代码中的 ...

  4. Django之forms表单类

    Form表单的功能 自动生成HTML表单元素 检查表单数据的合法性 如果验证错误,重新显示表单(数据不会重置) 数据类型转换(字符类型的数据转换成相应的Python类型) 1.创建Form类 from ...

  5. win10 1709正式版iso镜像下载|windows10 1709秋季创意者更新官方下载地址

    win10 1709正式版iso镜像下载|windows10 1709秋季创意者更新官方下载地址 发布时间:2017-10-18 14:27发布者:系统城-xtcjh浏览数:74458 win10 1 ...

  6. IPv4地址(一)概述

    IPv4地址的长度是多少? IPv4地址是如何表示的? IPv4地址的构成以及每一部分所起到的作用和占的位数特点? IPv4地址长度为32位. IPv4地址分为两部分:网络号和主机号 网络号部分惟一地 ...

  7. Array容易被忽略的join

    var lists, items = '', i; lists = [{ Fruits:'苹果' },{ Fruits:'香蕉' },{ Fruits:'菠萝' }]; /*items += '< ...

  8. Unity3D研究院之拓展自定义编辑器窗口

    Unity支持自行创建窗口,也支持自定义窗口布局.在Project视图中创建一个Editor文件夹,在文件夹中在创建一条脚本. 自定义窗口需要让脚本继承EditorWindow在设置MenuItem, ...

  9. 如何进行Web服务的性能测试?

    随着浏览器功能的不断完善,用户量不断的攀升,涉及到web服务的功能在不断的增加,对于我们测试来说,我们不仅要保证服务端功能的正确性,也要验证服务端程序的性能是否符合要求.那么性能测试都要做些什么呢?我 ...

  10. github commit, issue, pull request, project

    1 github的提供给用户操作和交流的几个对象 commit, issue, pull request and project 2 commit and commit comment commit就 ...