手记

php的foreach循环执行过程分析以及循环中执行unset()的一些问题

一、前言

      博主在最近操作数组的时候,在foreach循环中执行了unset操作,然后在后续的结果打印中,发现数据重复了。本来在博主的印象中,执行unset()后会除去数组中对应的值的,那么为什么这次会失败呢?下面咱们来详细看看foreach都是怎么执行的。

二、foreach的执行过程

      首先博主本地的php7.1版本的,也就是说有可能会和php5foreach不一样,所以咱们这边要分别从这两个版本来分析这些问题。

1、关于版本不同的foreach变化

首先我们要明确,php5php7对于foreach循环的使用原理是有差别的,参考如下:

       PHP 5foreach通过值遍历时还是会拷贝数组的. 但是PHP 7 内部实现这个迭代数组与 PHP 5 不同.PHP 7 不在依靠数组内部指针, 而PHP5是靠内部指针的. 验证PHP 5foreach下是否拷贝了数组

$arr = [0];
foreach ($arr as $k => $v) {
    debug_zval_dump($arr); // 当执行debug_zval_dump($var)的时候,$var会以传值的方式传递给debug_zval_dump,也就是会导致var的refcount加1
}

打印出来的refcount3, 说明在foreach中拷贝数组了, 导致refcount3. 进一步验证.

$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表现出来的不大一样

      通过以上的测试,我们可以发现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

1人推荐
随时随地看视频
慕课网APP