Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modifying an array during foreach() iteration

I have a couple of queries about modifying an array during a foreach() loop. In the code below I loop through three arrays that contain closures/callbacks and invoke each one. I append a closure to the end of each array during iteration, however sometimes foreach() doesn't seem to recognise that the array has changed size and so the appended closure doesn't get called.

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();

Output:

PHP: 5.3.14-1~dotdeb.0

a1 g() 
a2 h() callback 
a3

Expected output:

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

Output for $a3 if I uncomment the second element before the loop:

a3 a3 callback
  1. Why doesn't the first loop foreach ($this->a1 as &$v) realise $v has another element to iterate over?
  2. Why does modifying $a3 work during the third loop foreach ($a3 as &$x), but only when the array starts off with more than one element?

I realise modifying an array during iteration is probably not a good idea, but since PHP seems to allow it I'm curious why the above works the way it does.

like image 734
Gordon Freeman Avatar asked Jan 31 '13 19:01

Gordon Freeman


People also ask

Can you modify array in forEach?

Note that foreach does not modify the internal array pointer, which is used by functions such as current() and key(). It is possible to customize object iteration. In order to be able to directly modify array elements within the loop precede $value with &. In that case the value will be assigned by reference.

Does forEach modify?

The forEach method of an array does not modify the array, it just iterates over it. When you change the argument in the callback function, that doesn't affect the array either. Also, forEach doesn't do anything with the return values from the callback.

Can the elements of an array be updated using a forEach loop?

The forEach() loop takes the user defined function as on argument. This function has three parameters including the optional parameters. The first parameter is the value of the array which is to be updated while using in the forEach loop.

How do you modify an array?

You click the formula in the cell or formula bar and you can't change a thing. Array formulas are a special case, so do one of the following: If you've entered a single-cell array formula, select the cell, press F2, make your changes, and then press Ctrl+Shift+Enter..


1 Answers

Interesting observation:

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;
}

results in

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

This means:

  • a normal foreach loop operates on a copy of the array, any modifications of the array within the loop do not affect the loop
  • a foreach with referenced value is forced to use the original array but advances the array pointer before each iteration after assigning key and value variables. Also there is some optimization going on that prevents another check as soon as the pointer reaches the end. So at the beginning of the last iteration the loop is told to run once more and then finish - no more interfering possible.
  • a while loop with each() advances the array pointer just like foreach does but explicitly checks it again after the last iteration
  • a for loop where the array pointer is advanced after each iteration obviously has no problems with changing the array at any point.
like image 54
Fabian Schmengler Avatar answered Sep 22 '22 22:09

Fabian Schmengler