【问题标题】:Modifying an array during foreach() iteration在 foreach() 迭代期间修改数组
【发布时间】:2013-01-15 23:59:05
【问题描述】:

我有几个关于在 foreach() 循环期间修改数组的问题。在下面的代码中,我遍历三个包含闭包/回调的数组并调用每个数组。在迭代过程中,我在每个数组的末尾附加了一个闭包,但有时 foreach() 似乎无法识别出数组的大小已更改,因此不会调用附加的闭包。

class Foo
{
    private $a1 = array();
    private $a2 = array();

    public function f()
    {
        echo '<pre style="font-size: 20px;">';
        echo 'PHP: ' . phpversion() . '<br><br>';

        $this->a1[] = function() { echo 'a1 '; };
        $this->a1[] = array($this, 'g');
        foreach ($this->a1 as &$v)
        {
            // The callback added in g() never gets called.
            call_user_func($v);
            //echo 'count(v) = ' . count($v) . ' ';
        }

        echo '<br>';

        // The same thing works fine with a for() loop.
        $this->a2[] = function() { echo 'a2 '; };
        $this->a2[] = array($this, 'h');
        for ($i = 0; $i < count($this->a2); ++$i)
            call_user_func($this->a2[$i]);

        echo '<br>';

        // It also works fine using a local array as long as it
        // starts off with more than one element.
        $a3[] = function() { echo 'a3 '; };
        //$a3[] = function() { echo 'a3 '; };
        $i = 0;
        foreach ($a3 as &$x)
        {
            call_user_func($x);
            if ($i++ > 1) // prevent infinite loop
                break;

            // Why does this get called only if $a3 originally starts
            // with more than one element?
            $a3[] = function() { echo 'callback '; };
        }

        echo '</pre>';
    }

    private function g()
    {
        echo 'g() ';
        $this->a1[] = function() { echo 'callback '; };
    }

    private function h()
    {
        echo 'h() ';
        $this->a2[] = function() { echo 'callback '; };
    }
}

$foo = new Foo;
$foo->f();

输出:

PHP: 5.3.14-1~dotdeb.0

a1 g() 
a2 h() callback 
a3

预期输出:

a1 g() callback
a2 h() callback 
a3 callback

如果我取消注释循环前的第二个元素,$a3 的输出:

a3 a3 callback
  1. 为什么第一个循环 foreach ($this-&gt;a1 as &amp;$v) 没有意识到 $v 有另一个元素要迭代?
  2. 为什么在第三个循环 foreach ($a3 as &amp;$x) 期间修改 $a3 有效,但仅当数组以多个元素开始时才有效?

我意识到在迭代期间修改数组可能不是一个好主意,但由于 PHP 似乎允许这样做,我很好奇为什么上面的工作方式如此。

【问题讨论】:

  • 首先,您正在修改数组,同时它正在循环,而 PHP 的设计确实没有考虑到这一点。您最好在循环中执行回调函数并使用 foreach 而不引用。
  • @Hiroto 手册中提到了它,所以我认为 PHP 的设计考虑到了这一点。但是我意识到这可能不是一个好习惯,但我仍然想知道为什么它的行为如此,似乎不一致。
  • 我创建了一个快速课程,基本上完成了您的课程并得出了相同的结果。根据 php foreach 页面,while (list(, $value) = each($arr)) 应该与 foreach 完全相同,但是在使用 while 循环而不是 foreach 时我没有*问题。
  • 它提到它是因为它可以完成,但是 PHP 的 foreach 旨在复制数组并对其进行迭代,因为您不会以这种方式陷入无限循环。正如Rasmus Lerdorf 所说:“丑陋的问题往往需要丑陋的解决方案。以纯粹的方式解决丑陋的问题非常困难。”。 “优雅”的解决方案可能会使用回调函数,而不是依赖数组。

标签: php arrays foreach


【解决方案1】:

有趣的观察:

echo "foreach:  ";
$a = array(1,2,3);
foreach($a as $v) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nforeach&: ";
$a = array(1,2,3);
foreach($a as &$v) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nwhile:    ";
$a = array(1,2,3);
while(list(,$v) = each($a)) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nfor:      ";
$a = array(1,2,3);
for($v=reset($a); key($a)!==null; $v=next($a)) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

结果

foreach:  1 2 3 
foreach&: 1 2 3 4 
while:    1 2 3 4 5 
for:      1 2 3 4 5 

这意味着:

  • 普通的foreach 循环对数组的副本进行操作,循环内对数组的任何修改都不会影响循环
  • 具有引用值的foreach 被强制使用原始数组,但在分配键和值变量后每次迭代之前 推进数组指针。此外,还有一些优化正在进行,以防止指针到达末尾时立即进行另一次检查。因此,在最后一次迭代开始时,循环被告知再运行一次,然后结束 - 不再有干扰。
  • 带有each()while 循环像foreach 一样推进数组指针,但在最后一次迭代后再次显式检查它
  • 一个for 循环,其中数组指针在之后 每次迭代都提前,显然在任何时候更改数组都没有问题。

【讨论】:

    【解决方案2】:

    1.为什么第一个循环 foreach ($this->a1 as &$v) 没有意识到 $v 有另一个元素要迭代?

    这种行为看起来是由于在每次 foreach 迭代中数组上的内部指针都前进了。在数组的last迭代中,即当内部指针已经为空时,在数组末尾添加一个数组元素,意味着该元素将不会被迭代。通过对代码进行一些修改,可以看到这一点。

    class Foo
    {
        private $a1 = array();
        private $a2 = array();
    
        public function f()
        {
            echo '<pre style="font-size: 20px;">';
            echo 'PHP: ' . phpversion() . '<br><br>';
    
            $this->a1[] = function() { echo 'a1 <br/>'; };
            $this->a1[] = array($this, 'g');
            foreach ($this->a1 as $key => &$v)
            {
               //lets get the key that the internal pointer is pointing to 
               // before the call.
                      $intPtr = (key($this->a1) === null) ? 'null' : key($this->a1);
                    echo 'array ptr before key ', $key, ' func call is ',    
                           $intPtr, '<br/>' ;
                call_user_func($v);
                //echo 'count(v) = ' . count($v) . ' ';
            }
    
            echo '<br><br>';
    
            // The same thing works fine with a for() loop.
            $this->a2[] = function() { echo 'a2 '; };
            $this->a2[] = array($this, 'h');
            for ($i = 0; $i < count($this->a2); ++$i)
                call_user_func($this->a2[$i]);
    
            echo '<br><br>';
    
            // It also works fine using a local array as long as it
            // starts off with more than one element.
            $a3[] = function() { echo 'a3 '; };
            //$a3[] = function() { echo 'a3 '; };
            $i = 0;
            foreach ($a3 as &$x)
            {
                call_user_func($x);
                if ($i++ > 1) // prevent infinite loop
                    break;
    
                // Why does this get called only if $a3 originally starts
                // with more than one element?
                $a3[] = function() { echo 'callback '; };
            }
    
            echo '</pre>';
        }
    
        private function g()
        {
            echo 'g() <br>';
            $this->a1[] = function() { echo 'callback '; };
        }
    
        private function h()
        {
            echo 'h() <br>';
            $this->a2[] = function() { echo 'callback '; };
        }
    }
    
    $foo = new Foo;
    $foo->f(); 
    

    输出:

    array ptr before key 0 func call is 1
    a1 
    array ptr before key 1 func call is null <-will not iterate over any added elements!
    g() 
    
    a2 h() 
    callback 
    
    a3
    

    2.为什么在 foreach 的第三个循环($a3 as &$x)期间修改 $a3 有效,但仅当数组以多个元素开始时才有效?

    当然,如果您在内部指针返回 null 之前向数组添加一个元素,那么该元素将被迭代。在您的情况下,如果数组有一个元素,那么在第一次迭代中,内部指针已经返回 null。但是,如果最初有多个元素,则可以在第一次迭代时添加附加元素,因为此时内部指针将指向第二个初始元素。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-03-04
      • 2023-04-07
      • 1970-01-01
      • 1970-01-01
      • 2016-03-26
      • 2014-02-23
      • 2011-12-30
      • 2019-02-19
      相关资源
      最近更新 更多