Изменение массива во время foreach() итерации

У меня есть несколько запросов об изменении массива во время цикла 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
  • Почему первый цикл foreach ($this->a1 as &$v) не реализует $v имеет другой элемент для итерации?
  • Почему модификация $a3 работает во время третьего цикла foreach ($a3 as &$x), но только когда массив начинается с более чем одного элемента?

Я понимаю, что изменение массива во время итерации, вероятно, не очень хорошая идея, но поскольку PHP, похоже, позволяет это мне любопытно, почему это работает так, как это делается.

Ответ 1

1. Почему не первый цикл foreach ($ this- > a1 as & $v) реализует, что $v имеет другой элемент для итерации над?

Поведение выглядит потому, что внутренний указатель продвигается по массиву на каждой итерации 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 <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. Замените работу $a3 во время третьего цикла foreach ($ a3 as & $x), но только тогда, когда массив начинается с более чем одного элемента?

Конечно, если вы добавите элемент в массив до того, как внутренний указатель вернет null, тогда элемент будет итерирован. В вашем случае, если массив имеет один элемент, то на первой итерации внутренний указатель уже возвращает null. Однако, если изначально имеется более одного элемента, дополнительный элемент может быть добавлен на первой итерации, так как внутренний указатель будет указывать на второй элемент в это время.

Ответ 2

Интересное наблюдение:

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 работает с копией массива, любые модификации массива внутри цикла не влияют на цикл
  • a foreach с ссылочным значением принудительно использует исходный массив, но продвигает указатель массива перед каждой итерацией после назначения переменных ключа и значения. Также происходит некоторая оптимизация, которая предотвращает другую проверку, как только указатель достигает конца. Таким образом, в начале последней итерации цикл повторяется снова, а затем заканчивается - больше не мешает.
  • a while цикл с each() продвигает указатель массива точно так же, как foreach, но явно проверяет его снова после последней итерации
  • a for, где указатель массива продвигается после каждой итерации, очевидно, не имеет проблем с изменением массива в любой точке.