php foreach使用引用的陷阱

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了php foreach使用引用的陷阱相关的知识,希望对你有一定的参考价值。

最近工作中在foreach中使用引用的时候出现一个怪现象,使用2次foreach的时候数组值发生了改变,代码示例如下

1 <?php
2 $arr = array(‘1‘,‘2‘,‘3‘);
3 foreach($arr as &$row){
4 }
5 foreach($arr as $row){
6 }

 

我的预期结果是1,2,3 但是实际结果输出1,2,2
奇怪了,遍历数组难道还会改变数组的值么,猜测原因肯定出现在&row这个引用上。
在第2个循环里打印$arr
Array
(
[0] => 1
[1] => 2
[2] => 1
)
Array
(
[0] => 1
[1] => 2
[2] => 2
)
Array
(
[0] => 1
[1] => 2
[2] => 2
)
解释下具体的执行流程
1.第1个foreach结束,$row成为$arr[3]的引用
2.第2个foreach循环第1次的时候$row=1,所以此时$arr[3]=1,所以此时$arr=array(1,2,1),
3.依次类推,第2个foreach循环结束的时候$row=2,所以此时$arr=array(1,2,2);
在琢磨这个代码的时候,又翻了下php手册对引用的解释: 在 PHP中引用意味着用不同的名字访问同一个变量内容,这并不像 C 的指针:例如你不能对他们做指针运算,他们并不是实际的内存地址。
最接近的比喻是 Unix 的文件名和文件本身——变量名是目录条目,而变量内容则是文件本身。引用可以被看作是 Unix 文件系统中的硬链接(如果拿windows做比喻那就是快捷方式)。


看到这儿,在手册上又发现了一段代码

1 <?php
2 $array1 = array(1,2);
3 $x = &$array1[1]; // Unused reference
4 $array2 = $array1; // reference now also applies to $array2 !
5 $array2[1]=22; // (changing [0] will not affect $array1)
6 print_r($array1);

 

Produces:
Array
(
[0] => 1
[1] => 22 // var_dump() will show the & here
)
结果又出乎意料,注释掉$x = &$array1[1],改变$array2的值不影响$arry1的值,联想到php垃圾回收的引用计数器,每个php变量存在于zval容器里,zval容器除了包含变量的值和类型,还有2个额外的信息,
第1个是is_ref,是个bool值,用来标识变量是否是引用集合,通过这个我就知道这个变量是否是普通变量或者引用变量啦,第2个额外字段是refcount,表示指向这个zval容器的变量个数。

zval结构如下

typedef struct _zval_struct zval;
...
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};

 

$a = ‘ok‘;                                                                                       
xdebug_debug_zval(‘a‘);                                                                     
a:(refcount=1, is_ref=0),string ‘ok‘ (length=2)                                            

技术分享


$b = &$a;
xdebug_debug_zval(‘a‘);
xdebug_debug_zval(‘b‘);
a:(refcount=2, is_ref=1),string ‘ok‘ (length=2)
b:(refcount=2, is_ref=1),string ‘ok‘ (length=2)

技术分享

 

这时,引用次数是2,因为同一个变量容器被变量a和变量 b关联.当没必要时,php不会去复制已生成的变量容器,变量容器在"refcount"变成0时就被销毁
由于php内置函数debug_zval_dump不能看到zval的is_ref信息,所以这里使用xdebug_debug_zval,你需要安装xdebug扩展。

$a = ‘ok‘;
$b = $a;
$a = ‘no‘;

xdebug_debug_zval(‘a‘);
xdebug_debug_zval(‘b‘);

 

$a最后的值毫无疑问变成了‘no‘,php是怎么做的呢?
当执行$b=$a;的时候$a和$b指向了同一个zval容器,refcount=2,
最后改变$a的值的时候,会执行php的copy on write机制

PHP的copy on write机制:
php在修改一个变量以前,会检查refcount值,refcount大于1,php会复制一个新的zval,并将原来zval refcount减1,
并修改symbol_table(符号表),使得两个变量分离,这个机制就是所谓的copy on write(写时复制),

$a = ‘ok‘;
$b = &$a;
$b = ‘no‘;
$a,$b最后的值毫无疑问都变成了‘no‘,
执行到第2行的时候$a, $b指向同一个zval容器,refcount=2,is_ref=1,这时回执行php的change on write机制

PHP的change on write机制:
is_ref=1的时候,php会修改zval的值,但不会复制zval,这个过程称作(change on write:写时改变)


回到最开始手册上那个数组赋值的问题,array1和$array2

当考虑像 arrayobject这样的复合类型时,事情就稍微有点复杂. 与 标量(scalar)类型的值不同,数组和对象这类复合型的变量在生成zval时,会为每个item项生成一个zval容器。

$array1 = array(1,2);
$x = &$array1[1]; // Unused reference
$array2 = $array1; // reference now also applies to $array2 !
//xdebug_debug_zval(‘x‘);
xdebug_debug_zval(‘array1‘);
xdebug_debug_zval(‘array2‘);
print_r($array1);

 技术分享

(refcount=2, is_ref=0),
array (size=2)
0 => (refcount=1, is_ref=0),int 1
1 => (refcount=2, is_ref=1),int 2
array2:
(refcount=2, is_ref=0),
array (size=2)
0 => (refcount=1, is_ref=0),int 1
1 => (refcount=2, is_ref=1),int 2
Array ( [0] => 1 [1] => 2 )

可以看到$array1, $array2指向的是同一个zval,而且$array1[1], $array2[1],$x指向的也是同一个zval,

而且属性ref_count=1,所以修改$array2[1]的值的时候,$array1[1]的值也一起修改了.

如果去掉$x = &$array1[1];此时$array[1]指向的zval变量属性is_ref=0,然后执行$array2=$array1的时候,此时修改$array2[1]的值,

并不会影响到$array1[1]。

以上是关于php foreach使用引用的陷阱的主要内容,如果未能解决你的问题,请参考以下文章

php中引用&的一个小实例

php foreach引用赋值

说说PHP中foreach引用的一个坑

PHP foreach引用&

php的引用&

[PHP] foreach循环的引用赋值可能导致的问题