在PHP8.0
及之前的版本,foreach
有一个行为很容易让我们的代码写错,甚至成为很多公司的面试题:
1 |
|
输出结果如下:
1 | array(3) { |
我们可以理解&$value
是一个语法糖。
对于第一段foreach
,等价于:
1 | $array = [1, 2, 3]; |
这样,$value
其实是一个引用了。它在执行完三条赋值语句之后,指向着数组的最后一个元素($array[2])。
对于第二段foreach
,等价于:
1 | // $value 仍然引用着 $array[2]. |
因为$value
始终引用着$array[2]
,所以,修改$value
的同时,也会修改$array[2]
。所以,最后$array[2]
是2
,而不是3
。
为了解决这种犹如bug
的特性,PHP
提出了这个RFC
在这个RFC
里面,当跳出foreach
的时候,增加了一个opcode UNWRAP_REF
来解开引用。我们可以来看看对于上面这段代码,RFC
之后的opline
是啥:
1 | L0003 0000 ASSIGN CV0($array) array(...) |
我们发现,当$value
是引用的时候,会执行UNWRAP_REF
来解开引用。这样,$value
不再指向$array[2]
了。
OK
,到此为止,这个RFC
其实已经介绍完了。但是,我还想多说一些容易犯错的犹如bug
的特性:
1 |
|
执行这段代码,会输出如下内容:
1 | int(1) |
大家可能会奇怪为什么不会一直死循环的遍历$array
,毕竟我们在不断的给$array
末尾添加元素。
因为在遍历$array
之前,会创建一个$array
的副本(我们暂且叫做$array_2
吧),是由上面的FE_RESET_R
的实现了,但是此时只是增加对zend_array
的引用计数而已,即:
graph TB 1($array) --> 3[zend_array] 2($array_2) --> 3[zend_array]
接着,实际上遍历的是这个看不见的$array)2
。
当我们往$array[count($array)]
位置写数据的时候,$array
和$array_2
会发生写时分离。此时变成如下情况:
graph TB 1($array) --> 3[zend_array_1] 2($array_2) --> 4[zend_array_2]
所以,给$array
赋值,并不会影响我们遍历$array_2
。所以,当遍历3
次的时候就会停下来。
我们再来看一段代码:
1 |
|
这段代码就是死循环了,到最后我们会看到内存被耗尽:
1 | Fatal error: Allowed memory size of 134217728 bytes exhausted |
那么这是为什么呢?实际上,在遍历$array
的时候,也会创建一个副本$array_2
。但是,这里不是引用计数的关系了,而是引用的关系,也就意味着$array
就是$array_2
:
graph TB 1($array) --> 3[zend_ref] 2($array_2) --> 3[zend_ref] 3(zend_ref) --> 4[zend_array]
我们对$array
的修改,会影响到$array_2
,最终,foreach
会死循环。
所以,&$value
不但会让$value
自身成为引用变量,还会让$array_2
也变成引用。