|
|
|
|
移动端

为何说PHP引用是个坑,要慎用

关于引用的糟糕的设计决定,我个人最喜欢的一个例子是 PHP 自带的 sort() 函数。sort() 使用一个数组作为引用参数,然后通过引用返回一个排好序的数组。 像常规那样通过值返回一个排好序的数组可能还更好些。

作者:佚名来源:PHP编程|2018-04-15 22:17

开发者盛宴来袭!7月28日51CTO首届开发者大赛决赛带来技术创新分享

PHP引用(&)使用详解

php的引用(就是在变量或者函数、对象等前面加上&符号)

在PHP 中引用的意思是:不同的名字访问同一个变量内容。

与C语言中的指针是有差别的.C语言中的指针里面存储的是变量的内容,在内存中存放的地址。

1.变量的引用

PHP 的引用允许你用两个变量来指向同一个内容

  1. <?php 
  2.     $a="ABC"
  3.     $b =&$a; 
  4.     echo $a;//这里输出:ABC 
  5.     echo $b;//这里输出:ABC 
  6.     $b="EFG"
  7.     echo $a;//这里$a的值变为EFG 所以输出EFG 
  8.     echo $b;//这里输出EFG 
  9. ?> 

2.函数的引用传递(传址调用)

传址调用我就不多说了 下面直接给出代码

  1. <?php     
  2. function test(&$a){         
  3.       $a=$a+100; 
  4.     }     
  5.    $b=1;    echo $b;//输出1 
  6.     test($b);   //这里$b传递给函数的其实是$b的变量内容所处的内存         地址,通过在函数里改变$a的值 就可以改变$b的值了 
  7.     echo "<br>";    echo $b;//输出101?> 

2.函数的引用传递(传址调用)

传址调用我就不多说了 下面直接给出代码

  1. <?php     
  2. function test(&$a){         
  3.      $a=$a+100; 
  4.     }     
  5.     $b=1;    echo $b;//输出1 
  6.     test($b);   //这里$b传递给函数的其实是$b的变量内容所处的内存地址,通过在函数里改变$a的值 就可以改变$b的值了 
  7.     echo "<br>";    echo $b;//输出101?> 

要注意的是,在这里test(1);的话就会出错,原因自己去想。

引用(&)使用中的坑

在 PHP 中引用意味着用不同的名字访问同一个变量内容,不论你用哪个名字对变量做出了运算,其他名字访问的内容也将改变。

让我们通过代码来加深对此的理解。 首先我们写几个简单的语句,把一个变量赋值给另一个变量,并且改变另一个变量:

  1. <?php 
  2. $a = 23; 
  3. $b = $a; 
  4. $b = 42; 
  5. var_dump($a); // int(23) 
  6. var_dump($b); // int(42) 

这个脚本显示 $a 值仍然为 23  ,而 $b 则等于 42 。出现这个情况的原因是我们得到的是一个拷贝(具体发生了什么稍后讲解。。。)现在我们使用引用来做同样的事情:

  1. <?php 
  2. $a = 23; 
  3. $b = &$a; 
  4. $b = 42; 
  5. var_dump($a); // int(42) 
  6. var_dump($b); // int(42) 
  7. ?> 

现在 $a 的值也改变成了 42 。 事实上,$a 和 $b 之间没有任何区别,它们都使用了同一个变量容器(又名: zval )。 将这两者分开的唯一方法是使用 unset() 函数销毁其中任何一个变量。

在 PHP 中,引用不仅能用在普通语句中,还能用于函数参数和返回值:

  1. <?php 
  2. function &foo(&$param) { 
  3.  $param = 42; 
  4.  return $param; 
  5.   
  6. $a = 23; 
  7. echo "\$a before calling foo(): $a\n"
  8. $b = foo($a); 
  9. echo "\$a after the call to foo(): $a\n"
  10. $b = 23; 
  11. echo "\$a after touching the returned variable: $a\n"
  12. ?> 

你认为上面的结果是什么呢?—— 没错,就像下面这样:

  1. $a before calling foo(): 23 
  2. $a after the call to foo(): 42 
  3. $a after touching the returned variable: 42 

这里我们初始化了一个变量,并把它作为一个引用参数传给了一个函数。函数改变了它,它有了新值。该函数返回同一个变量,我们更改了返回的变量和它的原始值。。。 等等!它没变,不是吗!? —— 没错,可引用就是这样。 具体发生了如下事情:该函数返回了一个引用,引用了 $a 的变量容器 zval,并且通过 = 赋值操作符为它创建了一个副本。

为了修复这个问题,我们需要添加一个额外的 & 操作符:

  1. $b = &foo($a); 

结果和我们所期望的一样:

  1. $a before calling foo(): 23 
  2. $a after the call to foo(): 42 
  3. $a after touching the returned value: 23 

总结一下: PHP 的引用就是同一个变量的别名,想要正确的使用它们可能很难。想要详细了解引用计数,这里有份基础资料,请参阅 手册中的引用计数基本知识 。

PHP 5 发布时最大的变动是『对象处理方式』。一般我们理解为:

在 PHP 4 中,对象被当成变量来对待,所以当对象作为函数传参时,他们是被复制的。但在 PHP 5 中,他们永远是『引用传参』。

以上的理解并不完全正确。其主要目的是遵循『面对对象模式』:对象传参给函数或者方法后,这个函数发送一个指令给对象(例如调用了一个方法)以此来改变对象的状态(例如对象的属性)。因此传参进去的对象必须为同一个。 PHP 4 的面对对象用户使用『引用传参』来解决这个问题,不过很难做到完美。PHP 5 引进了独立于变量容器的『对象存储器』。当一个对象赋值给变量时,变量不再存储整个对象(属性表和其他的『类』信息),而是存储这个对象所在 存储器的引用 —— 当我们复制一个对象变量时,我们复制的是这个『存储器的引用』。这很容易被误解为『引用』,但是『存储器的引用』与『引用』是完全不同的概念。下面的示例代码有助于我们更好地区分:

  1. <?php 
  2. // 创建一个对象和此对象的引用变量 
  3. $a = new stdclass; 
  4. $b = $a; 
  5. $c = &$a; 
  6.   
  7. // 对『对象』进行操作 
  8. $a->foo = 42;  
  9. var_dump($a->foo); // int(42) 
  10. var_dump($b->foo); // int(42) 
  11. var_dump($c->foo); // int(42) 
  12.   
  13. // 现在直接改变变量的类型 
  14. $a = 42; 
  15. var_dump($a); // int(42) 
  16. var_dump($b); // object(stdClass)#1719 (1) { 
  17.     //   ["foo"]=> 
  18.     //   int(42) 
  19.     // } 
  20. var_dump($c); // int(42) 
  21. ?> 

以上代码中,修改对象的属性会影响到 复制 的变量 $b 和引用的变量 $c。但是在最后区块的代码中,当我们修改 $a 的类型时,引用的 $c 发生了变化,而复制得到的变量 $b 不会发生改变,这是个大多数有面对对象经验的工程师所期待的。

So, 面对对象是唯一使用『引用』的理由,但是现在 PHP 4 已死,你也可以放弃此类用法了。

另一个人们使用『引用』的理由是 —— 这将让代码更快。但是这是错误的,引用并不会使代码执行速度变快,更糟糕的是,很多时候『引用』会让你的代码执行效率更低。

我必须再郑重强调一次:是的,很多时候『引用』会让你的代码执行效率更低。

别的语言的工程师,他们阅读别的语言编码规范,会看到建议在处理大的数据结构或者字串时,使用指针来减小对内存的消耗以提高运行效率。这些工程师误将此概念理解到『引用』上,然而『指针』与『引用』是完全不同的技术模型。PHP 解析器与其他语言不同,在 PHP 中,我们使用『写时复制(copy-on-write)』模型。

在『写时复制』模型里,赋值和函数传参不会触发 复制 动作,你可以理解为多个不同的变量指向同一个『变量容器』,只有当『写』动作发生时,才会触发复制动作。这意味着,即使变量看起来像是『复制』的,本质上却不是。所以当传参一个巨大的变量给某个函数时,并不会对性能造成多大影响。不过此时如果你使用引用传参的话,引用传参会关闭『写时复制』机制,这会导致接下来那些没有使用引用的变量传参会被立刻复制一份。这也不是世界末日,你也可以在所有地方都引用就行了嘛。事实并非如此:PHP 的内部机制依赖于『写时复制』模型,存在很多你无法修改的内部函数传参。

我曾在某处看到过类似下面这样的代码:

  1. <?php 
  2. function foo(&$data) { 
  3.  for ($i = 0; $i < strlen($data); $i++) { 
  4.   do_something($data{$i}); 
  5.  } 
  6.   
  7. $string = "... looooong string with lots of data ....."
  8. foo(string); 
  9. ?> 

显然,上面这段代码的第一个问题是:在循环中调用 strlen() 而不是使用已经计算好的长度。也就是说调用一次 strlen($data) 就可以了的,但是他却调用了很多次。 不同于 C 这类语言, 一般来说,PHP 的字符串都自带了长度,因此也不用进行长度的计算。所以就 strlen() 而言,这还不算太糟糕。 但现在另一个问题是,案例中的这个开发者为了节省时间,传递了一个引用作为参数以显示自己的聪明。 然而,strlen() 期望得到的是一个副本。『写时复制』不能用于引用,因此 $data 将会在 strlen() 调用时被复制,strlen() 将会做一个绝对简单的操作 —— 事实上 strlen() 本来就是 PHP 里最简单的函数之一 —— 紧接着该副本就会被直接销毁。

如果没有使用引用,也就没必要进行复制操作,代码执行也会更快。而且就算 strlen() 支持引用,你也不会因此获得更多好处。

总的来说:

除了 PHP4 的遗留问题,不要在面向对象(OO)中使用引用。

不要使用引用来提升性能。

使用引用来完成事情的第三个问题是:通过参数的引用来返回数据所导致的糟糕的 API 设计。这个问题还是因为那个开发者没有意识到『PHP 就是 PHP 而不是其他语言』所导致的。

在 PHP 中,同一个函数可以返回不同数据类型。—— 因此,你可以在函数执行成功时返回一个字符串,而在失败时返回一个布尔值 false,PHP 也允许返回复杂的结构类型,比如数组和对象。所以在需要返回很多东西的时候,可以将他们打包在一起。另外,异常也是函数返回的一种方式。

使用引用是一件不好的事情,除了引用本身不好,并且还会使性能下降这个事实外,使用引用这种方式会使得代码难以维护。像下面这段代码的函数调用:

  1. do_something($var); 

你希望 $var 发生改变吗?—— 当然不会。然而,如果 do_something() 传递的参数是引用,它就可能会改变。

这类 API 的另一个问题是:函数不能链式调用,因而你总会遇到必须使用临时变量的场景。链式调用可能会使可读性降低,但是在许多场景下,链式调用使得代码更加简洁。

关于引用的糟糕的设计决定,我个人最喜欢的一个例子是 PHP 自带的 sort() 函数。sort() 使用一个数组作为引用参数,然后通过引用返回一个排好序的数组。 像常规那样通过值返回一个排好序的数组可能还更好些。当然,这么做是由于历史的原因:sort() 比『写时复制』更早出现。『写时复制』产生于 PHP4,而 sort() 则更早,它早在 PHP 还是作为一种在 Web 上做起事来很方便的东西,而不是真正的成为自己的语言的时候就存在了。

【编辑推荐】

  1. 如何在不会导致服务器宕机的情况下,用PHP读取大文件
  2. Nginx、FastCGI、PHP-fpm关系梳理
  3. Nginx服务器究竟是怎么执行PHP项目
  4. 【服务器】PHP如何实现依赖注入?
  5. PHP作为服务器端语言,有哪些优点?
【责任编辑:武晓燕 TEL:(010)68476606】


点赞 0
分享:
大家都在看
猜你喜欢
24H热文
一周话题
本月最赞

视频课程+更多

热门职位+更多

读 书 +更多

程序员面试宝典

本书取材于各大IT公司历年面试真题(笔试、口试、电话面试、英语面试,以及逻辑测试和智商测试)。详细分析了应聘程序员(含网络、测试等...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊