The Perils of Foreach, or “I’ve got yer references dangling right here!”
Here’s another one of those pitfalls in PHP that might be classified as a neophyte mistake, but nevertheless it bit me in the ass a while ago. I was perplexed at first why this one test I had written kept failing, and finally it boiled down to the way I was iterating through an array multiple times in the same block of code. As it turned out, I had made the mistake of leaving a reference to a variable hanging around after I was done with it. Later, in the same (unfortunately lengthy) block of code, I reused the variable name containing the reference for something else (assigned it a value), and it gave me some, shall we say, unexpected results.
Here, it’s easier to demonstrate with a short example. My bug was buried deep in some more convoluted code, this is just a simplified version of the same sort of thing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php $fruits = array( 'apple', 'banana', 'cherry' ); echo "foreach with assign by reference:\n"; foreach($fruits as &$fruit) { echo $fruit . "\n"; } echo "\nforeach with assign by value:\n"; foreach($fruits as $fruit) { echo $fruit . "\n"; } ?> |
The output for this looks something like this:
foreach with assign by reference:
apple
banana
cherry
foreach with assign by value:
apple
banana
banana
So yeah, you’ve got your fruits in alphabetical order, and the second time you go through the list you end up with two bananas and no cherries. The hell?
Well, I’ve seen an example like this one at various paces on the ‘net, usually from some poor confused soul trying to understand where the cherry went. Sometimes you even find a quick response that says how to fix the problem. I haven’t come across an examinantion of what’s really going on, though, so I figured I’d give it a whirl.Problem Number A: Dangling Reference
Let me just start by saying the example above is a very simplified example of the core problem. I’m sure some of you are wondering why I might bother writing code that uses references like this, but rather than dive into that right now, just know that I’m not insane and I had my reasons. Accepting the code as it stands, the first foreach loop is iterating through the $fruits array and assigning individual elements by reference. It works well enough – if you only ran that part of the program, you would never see a bug. However, there is a problem with it, and it is this: the $fruit reference remains even after the foreach loop is over. The variable $fruit points to a location that contains the value ‘cherry’. Also, because of the way references work, the third element of the $fruits array also points to the value ‘cherry’. Check it out:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php $fruits = array( 'apple', 'banana', 'cherry' ); echo "foreach with assign by reference:\n"; foreach($fruits as &$fruit) { echo $fruit . "\n"; } var_dump($fruits); $fruit = 'date'; var_dump($fruits); ?> |
foreach with assign by reference:
apple
banana
cherry
array(3) {
[0]=>
string(5) "apple"
[1]=>
string(6) "banana"
[2]=>
&string(6) "cherry"
}
array(3) {
[0]=>
string(5) "apple"
[1]=>
string(6) "banana"
[2]=>
&string(4) "date"
}
As you can see, the variable $fruit contains a reference, even after we’re done with it in the foreach loop. It points (or ‘refers’, if you like) back to a memory location that happens(just after the end of the loop) to contain the string ‘cherry’. Look closer, though – there’s something else going on that’s equally improtant. Notice the value of $fruits[2]. It, too, is a reference! That’s just the way references work – the moment you create a reference to a value, the original variable that contained the value also becomes a reference to that value. It’s one location in memory with two pointers; assign a new value to either and the other will point to the changed value as well.
Now, just to completely beat this dead horse, after the foreach loop, $fruit contains a reference, because that’s the last thing we assigned to it. PHP doesn’t care that the loop is over, it has no way of knowing whether or not you intend to use that reference value later on. When you assign the value ‘date’ to the variable $fruit, you’re putting the value ‘date’ into the location $fruit was referencing. The $fruits array is modified, ‘cherry’ becomes ‘date’.
Going back to the original example, it should now be (hopefully) apparent what’s going on – it’s really the same exact thing. Once the foreach by reference loop is over, $fruit contains a reference to a memory location that contains the string ‘cherry’. At the same time, $fruits[2] contains a reference to the same space. When we hit the second foreach loop, we’re iterating through the $fruits array and assiging that value to the location to which $fruit points, which (again) affects $fruits[2]. The 1st iteration, ‘apple’ is assigned. On the 2nd, it’s ‘banana’. At this point, $fruits[2] and $fruit both point to a location containing the string ‘banana’. On the last iteration, $fruits is assigned the value of whatever value is pointed to by $fruits[2] which was just changed to ‘banana’. Here’s a modified version of the original example with some different output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php $fruits = array( 'apple', 'banana', 'cherry' ); echo "foreach with assign by reference:\n"; foreach($fruits as &$fruit) { echo $fruit . "\n"; } echo "\nforeach with assign by value:\n"; $count = 0; foreach($fruits as $fruit) { echo "assigning value of \$fruits[$count] to \$fruit " . "and also \$fruits[2]: " . $fruits[$count] ."\n"; $count++; } echo "after 2nd foreach loop:\n"; var_dump($fruits); ?> |
foreach with assign by reference:
apple
banana
cherry
foreach with assign by value:
assigning value of $fruits[0] to $fruit and also $fruits[2]: apple
assigning value of $fruits[1] to $fruit and also $fruits[2]: banana
assigning value of $fruits[2] to $fruit and also $fruits[2]: banana
after 2nd foreach loop:
array(3) {
[0]=>
string(5) "apple"
[1]=>
string(6) "banana"
[2]=>
&string(6) "banana"
}
Finally, a Point
If you’ve managed to get this far, congratulations! The upshot of all this dancing around with references is this little nugget o’ advice:
Always unset a reference variable after you’re done with it, especially following a foreach loop!!!
If you don’t believe me, check out the php documentation. Doing so will destroy the reference, and prevent the kind of bug that can be quite the PITA to pin down. Here, then, is the example one last time, this time “fixed” with an unset call:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php $fruits = array( 'apple', 'banana', 'cherry' ); echo "foreach with assign by reference:\n"; foreach($fruits as &$fruit) { // etc. } unset($fruit); foreach($fruits as $fruit) { // etc. } echo "after 2nd foreach loop:\n"; var_dump($fruits); ?> |
And the blessed, correct output:
foreach with assign by reference:
after 2nd foreach loop:
array(3) {
[0]=>
string(5) "apple"
[1]=>
string(6) "banana"
[2]=>
string(6) "cherry"
}
