去年我参加了不少次会议,其中八次会议里我进行了相关发言,这其中我屡次谈到了 PHP 的引用问题,由于不少人对它的理解有所误差。在深刻讨论这个问题以前,咱们先回顾一下引用的基本概念,明确什么是“引用传递”。php
在 PHP 中引用意味着用不一样的名字访问同一个变量内容,不论你用哪一个名字对变量作出了运算,其余名字访问的内容也将改变。laravel
让咱们经过代码来加深对此的理解。 首先咱们写几个简单的语句,把一个变量赋值给另外一个变量,而且改变另外一个变量:数组
<?php $a = 23; $b = $a; $b = 42; var_dump($a); // int(23) var_dump($b); // int(42)
这个脚本显示 $a
值仍然为 23 ,而 $b
则等于 42 。出现这个状况的缘由是咱们获得的是一个拷贝(具体发生了什么稍后讲解。。。)如今咱们使用引用来作一样的事情:数据结构
<?php $a = 23; $b = &$a; $b = 42; var_dump($a); // int(42) var_dump($b); // int(42) ?>
如今 $a
的值也改变成了 42 。 事实上,$a
和 $b
之间没有任何区别,它们都使用了同一个变量容器(又名: zval
)。 将这二者分开的惟一方法是使用 unset()
函数销毁其中任何一个变量。函数
在 PHP 中,引用不只能用在普通语句中,还能用于函数参数和返回值:性能
<?php function &foo(&$param) { $param = 42; return $param; } $a = 23; echo "\$a before calling foo(): $a\n"; $b = foo($a); echo "\$a after the call to foo(): $a\n"; $b = 23; echo "\$a after touching the returned variable: $a\n"; ?>
你认为上面的结果是什么呢?—— 没错,就像下面这样:编码
$a before calling foo(): 23 $a after the call to foo(): 42 $a after touching the returned variable: 42
这里咱们初始化了一个变量,并把它做为一个引用参数传给了一个函数。函数改变了它,它有了新值。该函数返回同一个变量,咱们更改了返回的变量和它的原始值。。。 等等!它没变,不是吗!? —— 没错,可引用就是这样。 具体发生了以下事情:该函数返回了一个引用,引用了 $a
的变量容器 zval,而且经过 =
赋值操做符为它建立了一个副本。spa
为了修复这个问题,咱们须要添加一个额外的 &
操做符:.net
$b = &foo($a);
结果和咱们所指望的同样:设计
$a before calling foo(): 23 $a after the call to foo(): 42 $a after touching the returned value: 23
总结一下: PHP 的引用就是同一个变量的别名,想要正确的使用它们可能很难。想要详细了解引用计数,这里有份基础资料,请参阅 手册中的引用计数基本知识 。
PHP 5 发布时最大的变更是『对象处理方式』。通常咱们理解为:
在 PHP 4 中,对象被当成变量来对待,因此当对象做为函数传参时,他们是被复制的。但在 PHP 5 中,他们永远是『引用传参』。
以上的理解并不彻底正确。其主要目的是遵循『面对对象模式』:对象传参给函数或者方法后,这个函数发送一个指令给对象(例如调用了一个方法)以此来改变对象的状态(例如对象的属性)。所以传参进去的对象必须为同一个。 PHP 4 的面对对象用户使用『引用传参』来解决这个问题,不过很难作到完美。PHP 5 引进了独立于变量容器的『对象存储器』。当一个对象赋值给变量时,变量再也不存储整个对象(属性表和其余的『类』信息),而是存储这个对象所在 存储器的引用 —— 当咱们复制一个对象变量时,咱们复制的是这个『存储器的引用』。这很容易被误解为『引用』,可是『存储器的引用』与『引用』是彻底不一样的概念。下面的示例代码有助于咱们更好地区分:
<?php // 建立一个对象和此对象的引用变量 $a = new stdclass; $b = $a; $c = &$a; // 对『对象』进行操做 $a->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
不会发生改变,这是个大多数有面对对象经验的工程师所期待的。
So, 面对对象是惟一使用『引用』的理由,可是如今 PHP 4 已死,你也能够放弃此类用法了。
另外一我的们使用『引用』的理由是 —— 这将让代码更快。可是这是错误的,引用并不会使代码执行速度变快,更糟糕的是,不少时候『引用』会让你的代码执行效率更低。
我必须再郑重强调一次:是的,不少时候『引用』会让你的代码执行效率更低。
别的语言的工程师,他们阅读别的语言编码规范,会看到建议在处理大的数据结构或者字串时,使用指针来减少对内存的消耗以提升运行效率。这些工程师误将此概念理解到『引用』上,然而『指针』与『引用』是彻底不一样的技术模型。PHP 解析器与其余语言不一样,在 PHP 中,咱们使用『写时复制(copy-on-write)』模型。
在『写时复制』模型里,赋值和函数传参不会触发 复制 动做,你能够理解为多个不一样的变量指向同一个『变量容器』,只有当『写』动做发生时,才会触发复制动做。这意味着,即便变量看起来像是『复制』的,本质上却不是。因此当传参一个巨大的变量给某个函数时,并不会对性能形成多大影响。不过此时若是你使用引用传参的话,引用传参会关闭『写时复制』机制,这会致使接下来那些没有使用引用的变量传参会被马上复制一份。这也不是世界末日,你也能够在全部地方都引用就好了嘛。事实并不是如此:PHP 的内部机制依赖于『写时复制』模型,存在不少你没法修改的内部函数传参。
我曾在某处看到过相似下面这样的代码:
<?php function foo(&$data) { for ($i = 0; $i < strlen($data); $i++) { do_something($data{$i}); } } $string = "... looooong string with lots of data ....."; foo(string); ?>
显然,上面这段代码的第一个问题是:在循环中调用 strlen()
而不是使用已经计算好的长度。也就是说调用一次 strlen($data)
就能够了的,可是他却调用了不少次。 不一样于 C 这类语言, 通常来讲,PHP 的字符串都自带了长度,所以也不用进行长度的计算。因此就 strlen()
而言,这还不算太糟糕。 但如今另外一个问题是,案例中的这个开发者为了节省时间,传递了一个引用做为参数以显示本身的聪明。 然而,strlen()
指望获得的是一个副本。『写时复制』不能用于引用,所以 $data
将会在 strlen()
调用时被复制,strlen()
将会作一个绝对简单的操做 —— 事实上 strlen()
原本就是 PHP 里最简单的函数之一 —— 紧接着该副本就会被直接销毁。
若是没有使用引用,也就不必进行复制操做,代码执行也会更快。并且就算 strlen()
支持引用,你也不会所以得到更多好处。
总的来讲:
使用引用来完成事情的第三个问题是:经过参数的引用来返回数据所致使的糟糕的 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 知识社区