当前位置: 首页 > 后端技术 > PHP

PHP引用是个陷阱,慎用

时间:2023-03-30 06:14:25 PHP

去年参加了很多会议,有八个做了相关演讲。其中,我多次谈到PHP引用的问题,因为很多人都不是很了解。偏离了。在深入讨论这个问题之前,我们先回顾一下引用的基本概念,弄清楚什么是“引用传递”。引用在PHP中就是用不同的名字访问同一个变量内容,无论你用哪个名字对变量进行操作,其他名字访问的内容也会发生变化。让我们通过代码来更好地理解这一点。首先我们写一些简单的语句来将一个变量赋值给另一个变量并改变另一个变量:现在$a的值也变成了42。其实$a和$b没有区别,它们都使用相同的变量容器(又名:zval)。将两者分开的唯一方法是使用unset()函数销毁任一变量。在PHP中,引用不仅可以用在普通语句中,还可以用在函数参数和返回值中:你认为上面的结果是什么?——没错,像这样:$abeforecallingfoo():23$aafterthecalltofoo():42$aaftertouched返回的变量:42这里我们初始化一个变量,并作为一个引用参数传递到一个函数。函数改变了它,它有了新的价值。该函数返回相同的变量,我们更改返回的变量及其原始值。..ETC!没变,对吧!?—没错,这就是引用的目的。下面是发生的事情:该函数返回对$a的变量容器zval的引用,并通过=赋值运算符创建它的副本。为了解决这个问题,我们需要添加一个额外的&运算符:$b=&foo($a);结果是我们所期望的:调用foo()之前的$a:调用foo()之后的23$a:接触返回值之后的42$a:23总结一下:PHP引用是同一个变量的别名,它可能很难正确使用它们。如果你想了解更多关于引用计数的知识,这里有一份基础资料,参见手册中的引用计数基础知识。PHP5发布时最大的变化是“对象处理”。一般我们理解,在PHP4中,对象被当作变量对待,所以当对象作为参数传递给函数时,它们被复制。但在PHP5中,它们总是“通过引用传递”。以上理解并不完全正确。它的主要目的是遵循“面向对象模式”:将对象作为参数传递给函数或方法后,函数向对象发送指令(如调用方法)改变对象的状态(例如对象的属性)。因此,作为参数传入的对象必须相同。PHP4的面向对象用户使用“按引用传递”来解决这个问题,但很难做到尽善尽美。PHP5引入了与变量容器分开的“对象存储”。当一个对象被赋值给一个变量时,该变量不再存储整个对象(属性表和其他“类”信息),而是存储对象所在内存的引用——当我们复制一个对象变量时,我们所副本是这个“内存参考”。这很容易被误解为“引用”,但“内存的引用”和“引用”是完全不同的概念。下面的示例代码帮助我们更好的区分:foo=42;var_dump($a->foo);//int(42)var_dump($b->foo);//int(42)var_dump($c->foo);//int(42)//现在直接改变变量的类型$a=42;var_dump($a);//int(42)var_dump($b);//object(stdClass)#1719(1){//["foo"]=>//int(42)//}var_dump($c);//int(42)?>在上面的代码中,修改对象的属性会影响复制的变量$b和引用的变量$c。但是在最后一块代码中,当我们修改$a的类型时,引用的$c变了,而复制的变量$b却不会变,这是大多数有面向对象经验的工程师所期望的。所以,面向对象是使用“引用”的唯一理由,但现在PHP4已死,您也可以放弃这种用法。人们使用“引用”的另一个原因是——它会使代码更快。但这是错误的,引用并不能使代码执行得更快,更糟糕的是,很多时候“引用”会让你的代码执行效率降低。必须再次郑重强调:是的,很多时候“引用”会让你的代码效率低下。其他语言的工程师,在阅读其他语言的编码规范时,会看到建议在处理大型数据结构或字符串时,使用指针来减少内存消耗,提高运行效率。这些工程师把这个概念误解为“引用”,但“指针”和“引用”是完全不同的技术模型。PHP解析器不同于其他语言,在PHP中我们使用“写时复制”模型。在“copy-on-write”模型中,赋值和函数参数传递不会触发copy动作,可以理解为多个不同的变量指向同一个“变量容器”,只有当“write”动作发生时,copy将被触发动作。这意味着即使变量看起来被“复制”,但实际上并没有。因此,当将一个巨大的变量传递给一个函数时,它不会对性能产生太大影响。但此时如果使用引用传参,引用传参会关闭“copy-on-write”机制,导致后续不使用引用的变量参数被立即复制。这不是世界末日,你可以到处引用它。事实并非如此:PHP的内部机制依赖于“写时复制”模型,并且有许多您无法修改的内部函数参数。我在某处看到过这样的代码:显然,上面代码的第一个问题是:调用strlen()而不是使用已经计算好的长度。也就是说调用一次strlen($data)就够了,但是他调用了很多次。与C等语言不同,一般情况下,PHP字符串是有自己的长度的,所以不需要计算长度。所以就strlen()而言,还算不错。但是现在另一个问题是,在这种情况下,开发人员通过将引用作为参数传递来显示他的聪明才智,以节省时间。但是,strlen()需要一个副本。“copy-on-write”不能用于引用,所以调用strlen()时会复制$data,strlen()会做一个绝对简单的操作——实际上strlen()本来就是PHP中最简单的函数——那么副本就直接销毁了。如果不使用引用,就不需要复制操作,代码执行速度会更快。即使strlen()支持引用,您也不会从中获益太多。一般而言:不要在面向对象(OO)中使用引用,除非作为PHP4的遗留物。不要使用引用来提高性能。使用引用来完成任务的第三个问题是通过引用参数返回数据导致的API设计不佳。这个问题仍然是开发者没有意识到“PHP是PHP,不是其他语言”造成的。在PHP中,同一个函数可以返回不同的数据类型。-因此,可以在函数执行成功时返回一个字符串,失败时返回一个布尔值false。PHP还允许返回复杂的结构类型,例如数组和对象。所以当你需要退回的东西很多的时候,可以把它们打包在一起。此外,异常也是函数返回的一种方式。使用引用是一件坏事,除了引用本质上是不好的并且会降低性能之外,以这种方式使用引用会使代码难以维护。像这样的函数调用:do_something($var);你想改变$var吗?-当然不是。但是,如果传递给do_something()的参数是一个引用,它可能会改变。这种类型的API的另一个问题是函数不能链接,所以你总是会遇到必须使用临时变量的情况。链接可能会降低可读性,但在很多场景下,链接会让代码更简洁。关于引用的错误设计决策,我个人最喜欢的例子之一是PHP的本机sort()函数。sort()将一个数组作为引用参数,并返回一个按引用排序的数组。像往常一样按值返回排序数组可能会更好。当然,这样做是出于历史原因:sort()早于“写时复制”。“写时复制”产生于PHP4,sort()更早。早在PHP就已经存在的时候,还是一个方便在Web上做事的东西,而不是真正成为自己的语言。.总之:PHP中的引号不好。不要使用引号。它们只会造成麻烦,并且不会让您对使用引用来提升引擎抱有希望。如需更多现代PHP知识,请访问Laravel/PHP知识社区