February 11, 2009

PHP array, foreach loop with references and no block-scope – a killer recipe

Today morning I was browsing through my collection ridiculous php bugs ( I have 20+ such code snippet now) and my eyes got stuck to this problem. I cannot remember from where I got this one but surely it’s a jewel in my collection.

Let give a look to the original code
<?php

   $arr = array('A', 'B', 'C', 'D', 'E');
   foreach ($arr as &$val) {}
   foreach ($arr as $val) {}
   var_dump($arr);

?>

The problem is that the var_dump gives a result

array(5) {
  [0]=>
  &string(1) "A"
  [1]=>
  &string(1) "B"
  [2]=>
  &string(1) "C"
  [3]=>
  &string(1) "D"
  [4]=>
  &string(1) "D"
}

Shocked! Aaha… who screwed up your array? Apparently it’s your references
and variable scopes. You get it right but you will be biased if you don’t
criticize the lack of block-scope in php.
In the first foreach block we are assigning reference of the each element
to $val. Thus at the end of the first foreach loop $val actually a reference
to the final index of array $arr.
Now as there is no concept of block-scope in php, the $val will retain the
reference to the final index $arr at the start of the second loop (pathetic!).
 We can simply represent the situation as bellow

<?php

   $arr = array('A', 'B', 'C', 'D', 'E');
   $val = &$arr[4] ;
   foreach ($arr as $val) {}
   var_dump($arr);

?>

Now doing a var_dump of $arr inside the second foreach loop gives us the
following result.
array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'A',
)

array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'B',
)

array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'C',
)

array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'D',
)

array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'D',
) 
So it is clear now the second foreach loop actually assigns value of
each index one by one to the index referred by $val (final index).
 Now try out this solution 

<?php

   $arr = array('A', 'B', 'C', 'D', 'E');
   foreach ($arr as &$val) {}
   unset($val);
   foreach ($arr as $val) {}
   var_dump($arr);

?>

IT WORKS :)

(c) Sourav Ray Creative Commons License

3 comments:

  1. My friend sysop073 [http://www.reddit.com/user/sysop073/] has put a comment on the post in reddit, explaining a C equivalent of the above code.

    Here is the equivalent C code
    -----------
    char arr[] = {'A', 'B', 'C', 'D', 'E'};
    char* val;
    // Treat val as a pointer:
    for(int i = 0; i < 5; i++) {val = arr + i;}
    // Treat val as a normal variable:
    for(int i = 0; i < 5; i++) {*val = arr[i];}
    // arr is now {'A', 'B', 'C', 'D', 'D'}
    ---------
    Thanks sysop073!

    ReplyDelete
  2. This is documented at: http://php.net/foreach
    Try find unset() in that page.. :)

    ReplyDelete
  3. @Jani
    Thank you for your comment. The PHP documentation mention only a warning message "Reference of a $value and the last array element remain even after the foreach loop. It is recommended to destroy it by unset()". They are not elaborated the possible consequences. The point I want to made in this article is, "PHP variables should have block scope".

    Mean while I got a mail from Rick Fletcher, who have originally reported the bug before the Warning Message is added on the PHP Document. In the mail he said "... I've given up hope that they'll ever come to their senses and fix it. It's second nature now to include that unset() call any time I use a foreach loop".
    @rick ... Thank You!

    ReplyDelete