Wednesday, February 17, 2010

PHP: References To Array Elements Are Risky

References to array elements can bite! And it is not only the case with referencing in foreach loop. It seems that creating a reference to an array element replaces that element itself with a reference. If you then copy such an array and change the elements inside copy you can overwrite original value!

All of the presented code was tested on Mac (PHP 5.2.11), Linux (PHP 5.2.6-1+lenny4) and Windows XP (PHP 5.3.0) using PHP cross platform testing lab on Mac based on VirtualBox.

Changing copy affects original array

Sounds impossible? I agree. I couldn't believe it myself. Nevertheless here is the proof:
Example 1
$a = array('one', 'two', 'three', 'four'); 
$a2 = &$a[2]; 
 
$b = $a; 
$b[1] = 'two again'; 
$b[2] = 'reference bites'; 
 
var_dump($a, $b);
The above example will output:
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  &string(15) "reference bites"
  [3]=>
  string(4) "four"
}
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(9) "two again"
  [2]=>
  &string(15) "reference bites"
  [3]=>
  string(4) "four"
}

Changing copy of a copy affects original array too

If you look at the dump carefully you will see that $a[2] and $b[2] are displayed as references here. That would mean the element has been replaced with a reference. And if you copy a reference you just get what? Same reference, right? So any copy of $a would contain that reference. Going forward any copy of a copy of $a would contain that same reference. Let's check it:
Example 2
$a = array('one', 'two', 'three', 'four'); 
$a2 = &$a[2]; 
 
$b = $a; 
$c = $b; 
$c[2] = 'references bites more'; 
$b[1] = 'two again'; 
$a[0] = 'one more'; 
var_dump($a, $b, $c);
The above example will output:
array(4) {
  [0]=>
  string(8) "one more"
  [1]=>
  string(3) "two"
  [2]=>
  &string(21) "references bites more"
  [3]=>
  string(4) "four"
}
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(9) "two again"
  [2]=>
  &string(21) "references bites more"
  [3]=>
  string(4) "four"
}
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  &string(21) "references bites more"
  [3]=>
  string(4) "four"
}

Tricky "foreach" with reference explained

As you can see, the only element affected is the one referenced. That can lead to serious potential problems. Working on a copy of an array with scalar elements seems to be not as safe as one may thought. However, that explains one of the biggest pitfalls of iterating arrays in PHP: foreach with reference. The following is rather common knowledge:
Example 3
$a = array('one', 'two', 'three', 'four'); 
foreach ($a as &$v) {} 
foreach ($a as $v) {} 
var_dump($a);
The above example will output:
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  string(5) "three"
  [3]=>
  &string(5) "three"
}
But what was the explanation for that, again? It is quite clear that $v keeps reference to $a[3] after the foreach loop is finished. So what values does $v (effectively $a[3]) get within the next foreach loop? Let's see:
Example 4
$a = array('one', 'two', 'three', 'four'); 
foreach ($a as &$v) {} 
foreach ($a as $v) { 
 var_dump($a[3]); 
}
The above example will output:
string(3) "one"
string(3) "two"
string(5) "three"
string(5) "three"
Well, it's quite obvious. Or is it? As the foreach manual page states: "unless the array is referenced, foreach operates on a copy of the specified array and not the array itself". I would assume that "array is referenced" means array elements are referenced like this: foreach ($a as &$v) {} Apparently that is not the case with the second loop and it should work on a copy. Let's have a look what exactly would happen if the second foreach loop really worked on a solid copy:
Example 5
$a = array('one', 'two', 'three', 'four'); 
$b = $a; 
foreach ($a as &$v) {} 
foreach ($b as $v) {} 
var_dump($a);
The above example will output:
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  string(5) "three"
  [3]=>
  &string(4) "four"
}
I imagine you did expect this result. Beware references to array elements and thanks for reading. I hope you found this article useful. Please don't think twice before you leave your comment.

1 comments:

  1. Using the latest version of PHP, instead of in example 5 doing the $b = $a, I instead was able to work around the issue by changing the $v in the second foreach loop to a different variable. It seems that using the same value variable that you used as the reference variable is what is not working as expected.

    ReplyDelete