isekaijin
isekaijin

Reputation: 19742

How does the foreach control structure behave when the array/collection being iterated is modified inside the loop?

I have written the following function to flatten arrays:

function flatten() {
    $args  = func_get_args();
    $items = array();

    for ($i = 0; $i < count($args); $i++) {  // <-- (*)
        $arg =& $args[$i];

        if (is_array($arg))
            foreach ($arg as &$item)
                $args[] =& $item;
        else
            $items[] = $arg;
    }

    return $items;
}

I would like to replace the for line with the simple foreach ($args as &$arg). On what basis? I once wrote a class that implements the Iterator interface, which is essentially the basis of how the foreach control structure works. If I remember correctly, what the foreach control structure does is the following:

  1. Use the rewind() method to set an internal index variable to the position of the first element.
  2. Use the valid() method to test whether the end of the array has already been reached. If so, exit.
  3. Use the key() and current() methods of the `Iterator interface to retrieve the key and value of the current element of the array.
  4. Use the next() method to set the internal index variable to the position of the element immediately after the current one.
  5. Goto 2.

At least, that is how it works with user-defined classes. I am not so sure about how it would work with the built-in array type. Would it work the same way? Can I replace the for line with the foreach?

Upvotes: 1

Views: 432

Answers (3)

webbiedave
webbiedave

Reputation: 48897

$nums = array(1, 2, 3, 4);

$newNum = max($nums) + 1;
foreach ($nums as $num) {
    echo $num;
    if ($newNum > 10) {
        break;
    }
    $nums[] = $newNum++;
}

print_r($nums);

/* output
1234

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 4
    [4] => 5
    [5] => 6
    [6] => 7
    [7] => 8
)
*/

Using reference foreach ($nums as &$num):

/* output
1234567

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 4
    [4] => 5
    [5] => 6
    [6] => 7
    [7] => 8
    [8] => 9
    [9] => 10
)
*/

Upvotes: 1

Michael Edenfield
Michael Edenfield

Reputation: 28338

First, I should point out that, in general, modifying the contents of a collection while iterating over it is considered a "bad thing" -- many languages will throw an exception if you try. PHP is not one of those languages, however.

Two things are relevant here for your question:

First, when using arrays, PHP's foreach makes a copy of the array and iterates over that. In this case, you can safely modify the original array but your foreach won't see any of those changes. In your case that wouldn't work -- the new values you tack onto the end of $args won't come up in the foreach.

You can force PHP to use the original array by iterating over a reference to the array. In this case, the internal behavior will become relevant. PHP keeps an internal pointer to the "next" array element. If you change the contents of an array element that 'foreach' has already seen, you will not see the changes. If you change the contents of the array somewhere beyond the current element, you will see those changes. This should work for you, but I dunno if I'd trust it.

Upvotes: 2

Francis Lewis
Francis Lewis

Reputation: 8980

for and foreach are pretty interchangeable, but in this case, you cannot add elements to the args array and process them right away with the foreach, so your function will not function the same way if you use foreach.

A couple things to point out about the code: 1) putting the count($args) inside the 2nd parameter of the for loop means it's processed every iteration through the loop, which if you have a really large array can be expensive.

I would count the number of args before processing the loop, store it in a variable and use that in place of count($args) in the for arguments, then add 1 to the count every time you add a new element to the args array. This would be faster and use less memory.

2) This could be cleaned up to use recursion through functions, which would do the same thing without multiple loops and would use slightly less code.

Upvotes: 1

Related Questions