一、前言
博主在最近操作数组的时候,在foreach
循环中执行了unset
操作,然后在后续的结果打印中,发现数据重复了。本来在博主的印象中,执行unset()
后会除去数组中对应的值的,那么为什么这次会失败呢?下面咱们来详细看看foreach
都是怎么执行的。
二、foreach的执行过程
首先博主本地的php
是7.1
版本的,也就是说有可能会和php5
的foreach
不一样,所以咱们这边要分别从这两个版本来分析这些问题。
1、关于版本不同的foreach变化
首先我们要明确,php5
和php7
对于foreach
循环的使用原理是有差别的,参考如下:
PHP 5
在foreach
通过值遍历时还是会拷贝数组的. 但是PHP 7
内部实现这个迭代数组与 PHP 5
不同.PHP 7
不在依靠数组内部指针, 而PHP5
是靠内部指针的. 验证PHP 5
在foreach
下是否拷贝了数组
$arr = [0];
foreach ($arr as $k => $v) {
debug_zval_dump($arr); // 当执行debug_zval_dump($var)的时候,$var会以传值的方式传递给debug_zval_dump,也就是会导致var的refcount加1
}
打印出来的refcount
为3, 说明在foreach
中拷贝数组了, 导致refcount
为3. 进一步验证.
$arr = [0];
foreach ($arr as $v) {
$copy = $arr;
debug_zval_dump($arr);
}
假设数组在循环中拷贝了, 那么refcount
应该为4, 其打印结果跟我猜想一样. 说明数组在foreach
进行拷贝了. 而且不受数组的长度影响. 因为数组长度为2时候, 还是打印4.在PHP5 foreach
靠的是数组指针在移动从而达到迭代数组的值。
PHP7
升级手册中提及了"foreach 通过值遍历时,操作的值为数组的副本. 当默认使用通过值遍历数组时,foreach 实际操作的是数组的迭代副本,而非数组本身。 "这就意味着,foreach
中的操作不会修改原数组的值。
$arr = [0];
foreach ($arr as $k => $v) {
debug_zval_dump($arr);
}
// 结果是1,和php 5表现出来的不大一样
debug_zval_dump()参考手册:https://www.php.net/manual/zh/function.debug-zval-dump.php
通过以上的测试,我们可以发现php7
在循环的时候,只是拷贝了一个副本,然后下面的操作都是对于副本来说的,并不是一直拷贝原来的$arr
数组。
2、关于引用计数
引用计数这块总是容易搞混的,这块设计到php
的设计,我们必须要请出鸟哥来解释这个问题,参考鸟哥博客:
深入理解PHP原理之变量分离/引用(Variables Separation)
例子展示:
<?php
$var = 1;
$var_dup = $var;
?>
第一行,创建了一个整形变量,变量值是1。 此时保存整形1的这个zval的refcount为1。
第二行,创建了一个新的整形变量,变量也指向刚才创建的zval,并将这个zval的refcount加1,此时这个zval的refcount为2。
3、探寻foreach的运行过程
在测试的时候,发现有个博主的例子更好,只不过是php5
下的例子,博主这边以php7
为例,执行下面代码:
$arr = array('a','b','c');
foreach ($arr as $key=> $value) {
$arr[] = 'd';
print_r($arr);
var_dump($key,$value);
}
var_dump(current($arr));
var_dump($arr);
打印结果:
$ Array
[0] => a
[1] => b
[2] => c
[3] => d
)
int(0)
string(1) "a"
Array
(
[0] => a
[1] => b
[2] => c
[3] => d
[4] => d
)
int(1)
string(1) "b"
Array
(
[0] => a
[1] => b
[2] => c
[3] => d
[4] => d
[5] => d
)
int(2)
string(1) "c"
string(1) "a" //输出当前指针对应的数组值
array(6) {
[0]=>
string(1) "a"
[1]=>
string(1) "b"
[2]=>
string(1) "c"
[3]=>
string(1) "d"
[4]=>
string(1) "d"
[5]=>
string(1) "d"
}
通过打印结果我们可以发现:
(1)foreach循环的时候,确实是复制了一份$arr,循环的是这个副本,循环到“c”就结束了本次循环
(2)当循环结束之后,副本会重新赋值给$arr,所以我们打印循环后的$arr会返回赋值后的数组
(3)php7在循环的时候,并没有影响到数组的指针,指针并不是像php5一样停留在最后一个值的右侧。
php7的指针仍旧指向数组的第一位。
关于php5
的结论,大家可以自己在本地试试,也可以参考下面的链接,博主这边不方便再调到php5
去测试。还是执行上面这些测试代码,不过结果会大不相同。
三、foreach中执行unset()的问题
了解完foreach
的执行过程之后,我们心里应该对这个问题就有点数了,下面详细说一下出现bug的情况:
1、当我们是为了筛选数组的时候
foreach($arr as $k=>$v){
unset($arr[0]);
}
var_dump($arr)
我们会发现打印出来的数组确实会变少,并不会有什么bug
出现。
2、当我们unset()之后还要执行一些操作的时候
$arr = array(1,2,3,4,1);
foreach($arr as $k=>$v){
unset($arr[0]);
if($v == 1){
$new[] = $v;
}
}
var_dump($arr);
var_dump($new);
打印结果:
array(4) {
[1]=>
int(2)
[2]=>
int(3)
[3]=>
int(4)
[4]=>
int(1)
}
array(2) {
[0]=>
int(1)
[1]=>
int(1)
}
3、总结
我们可以看到,新的$new
数组并不是和我们想象的那样,排除$arr[0]
,只保留$arr[4]
。这就是我们上面说的,循环的时候循环的是副本,并不会直接影响到数组,所以在循环体内进行的unset()
在循环中是只对副本有用的。而当循环结束之后,把副本赋值给$arr
,所以打印的结果就是少了$arr[0]
。
博主碰到的bug
就是由于在循环中unset()
之后,以为已经去掉了$arr[0]
,所以下面在统计数组中其他值的时候,实际上是把$arr[0]
算上去的,因此数据出现了重复。解决此类问题建议是分开foreach
循环,先筛选,再统计数据,这样数据准确性会好一些。
end