关于引用的糟糕的设计决定, 我个人最喜欢的一个例子是 PHP 自带的 sort() 函数. sort() 使用一个数组作为引用参数, 然后通过引用返回一个排好序的数组. 像常规那样通过值返回一个排好序的数组可能还更好些.
PHP 引用 (&) 使用详解
php 的引用(就是在变量或者函数, 对象等前面加上 & 符号)
在 PHP 中引用的意思是: 不同的名字访问同一个变量内容.
与C语言中的指针是有差别的.C语言中的指针里面存储的是变量的内容, 在内存中存放的地址.
1. 变量的引用
PHP 的引用允许你用两个变量来指向同一个内容
- $a="ABC";
- $b =&$a;
- echo $a;// 这里输出: ABC
- echo $b;// 这里输出: ABC
- $b="EFG";
- echo $a;// 这里 $a 的值变为 EFG 所以输出 EFG
- echo $b;// 这里输出 EFG
- ?>
2. 函数的引用传递(传址调用)
传址调用我就不多说了 下面直接给出代码
- function test(&$a){
- $a=$a+100;
- }
- $b=1; echo $b;// 输出1
- test($b); // 这里 $b 传递给函数的其实是 $b 的变量内容所处的内存 地址, 通过在函数里改变 $a 的值 就可以改变 $b 的值了
- echo "
- "; echo $b;// 输出 101?>
2. 函数的引用传递(传址调用)
传址调用我就不多说了 下面直接给出代码
- function test(&$a){
- $a=$a+100;
- }
- $b=1; echo $b;// 输出1
- test($b); // 这里 $b 传递给函数的其实是 $b 的变量内容所处的内存地址, 通过在函数里改变 $a 的值 就可以改变 $b 的值了
- echo "
- "; echo $b;// 输出 101?>
要注意的是, 在这里 test(1); 的话就会出错, 原因自己去想.
引用 (&) 使用中的坑
在 PHP 中引用意味着用不同的名字访问同一个变量内容, 不论你用哪个名字对变量做出了运算, 其他名字访问的内容也将改变.
让我们通过代码来加深对此的理解. 首先我们写几个简单的语句, 把一个变量赋值给另一个变量, 并且改变另一个变量:
- $a = 23;
- $b = $a;
- $b = 42;
- var_dump($a); // int(23)
- var_dump($b); // int(42)
这个脚本显示 $a 值仍然为 23 , 而 $b 则等于 42 . 出现这个情况的原因是我们得到的是一个拷贝 (具体发生了什么稍后讲解...) 现在我们使用引用来做同样的事情:
- $a = 23;
- $b = &$a;
- $b = 42;
- var_dump($a); // int(42)
- var_dump($b); // int(42)
- ?>
现在 $a 的值也改变成了 42 . 事实上,$a 和 $b 之间没有任何区别, 它们都使用了同一个变量容器(又名: zval ). 将这两者分开的唯一方法是使用 unset() 函数销毁其中任何一个变量.
在 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, 并且通过 = 赋值操作符为它创建了一个副本.
为了修复这个问题, 我们需要添加一个额外的 & 操作符:
$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 引进了独立于变量容器的对象存储器. 当一个对象赋值给变量时, 变量不再存储整个对象(属性表和其他的类信息), 而是存储这个对象所在 存储器的引用 -- 当我们复制一个对象变量时, 我们复制的是这个存储器的引用. 这很容易被误解为引用, 但是存储器的引用与引用是完全不同的概念. 下面的示例代码有助于我们更好地区分:
- // 创建一个对象和此对象的引用变量
- $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 的内部机制依赖于写时复制模型, 存在很多你无法修改的内部函数传参.
我曾在某处看到过类似下面这样的代码:
- 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() 支持引用, 你也不会因此获得更多好处.
总的来说:
除了 PHP4 的遗留问题, 不要在面向对象 (OO) 中使用引用.
不要使用引用来提升性能.
使用引用来完成事情的第三个问题是: 通过参数的引用来返回数据所导致的糟糕的 API 设计. 这个问题还是因为那个开发者没有意识到PHP 就是 PHP 而不是其他语言所导致的.
在 PHP 中, 同一个函数可以返回不同数据类型.-- 因此, 你可以在函数执行成功时返回一个字符串, 而在失败时返回一个布尔值 false,PHP 也允许返回复杂的结构类型, 比如数组和对象. 所以在需要返回很多东西的时候, 可以将他们打包在一起. 另外, 异常也是函数返回的一种方式.
使用引用是一件不好的事情, 除了引用本身不好, 并且还会使性能下降这个事实外, 使用引用这种方式会使得代码难以维护. 像下面这段代码的函数调用:
do_something($var);
你希望 $var 发生改变吗?-- 当然不会. 然而, 如果 do_something() 传递的参数是引用, 它就可能会改变.
这类 API 的另一个问题是: 函数不能链式调用, 因而你总会遇到必须使用临时变量的场景. 链式调用可能会使可读性降低, 但是在许多场景下, 链式调用使得代码更加简洁.
关于引用的糟糕的设计决定, 我个人最喜欢的一个例子是 PHP 自带的 sort() 函数. sort() 使用一个数组作为引用参数, 然后通过引用返回一个排好序的数组. 像常规那样通过值返回一个排好序的数组可能还更好些. 当然, 这么做是由于历史的原因: sort() 比写时复制更早出现.写时复制产生于 PHP4, 而 sort() 则更早, 它早在 PHP 还是作为一种在 web 上做起事来很方便的东西, 而不是真正的成为自己的语言的时候就存在了.
来源: http://server.51cto.com/sOS-570616.htm