As I wrote some code, PHP confused me a little as I didn't expected the result of the following code:
$data = array(array('test' => 'one'), array('test' => 'two'));
foreach($data as &$entry) {
$entry['test'] .= '+';
}
foreach($data as $entry) {
echo $entry['test']."\n";
}
I think it should output
one+
two+
However the result is: http://ideone.com/e5tCsi
one+
one+
Can anyone explain to me why?
This is expected behaviour, see also https://bugs.php.net/bug.php?id=29992.
The reference is maintained when using the second foreach, so when using the second foreach the value of $entry, which points still to $data[1], is overwritten with the first value.
P.s. (thanks to #billyonecan for saying it): you need to unset($entry) first, so that your reference is destroyed.
This is mentioned specifically in the documentation for foreach. You should unset the loop variable when it gets elements of the array by reference.
Warning
Reference of a $value and the last array element remain even after the
foreach loop. It is recommended to destroy it by unset().
Related
I'm testing the performances/hitches for References over copies of an array. I have the following code:
function ScoreWords($Value){
$WordList = array(
"Amazing" => 1,
"Value" => 300,
"Elements" => 30,
"Another" => 0
);
if (array_key_exists($Value,$WordList)){
return $WordList[$Value];
}
}
$array = ["Value","Another",1,2,3,4];
echo implode(',', $array), "<br>";
foreach ($array as &$value) {
ScoreWords($value);
}
echo implode(',', $array), "<br>";
foreach ($array as $value) {
ScoreWords($value);
}
echo implode(',', $array), "<br>";
But it seems, the code pasted above works semi-fine. Output is:
Value,Another,1,2,3,4
Value,Another,1,2,3,4
Value,Another,1,2,3,3
I found this by mistake as imploding was not actually necessary, but this sparks the question. Why is there a duplicated value for the final print rather than the correct value of 4? regardless of what the content of the array is. It seems to duplicate the second from last element as the last element?
What is happening is that after your 1st foreach, $value is a reference to the last element in the array. As that loop progressed it was a reference to each element, until finally stopping at the last one.
So, when the 2nd foreach runs, $value is still a reference. As that loop runs, it updates $value, which in turn, updates the last element in the array.
When it gets to the last element, it was set to 3 from the previous loop iteration. So, that's why it's set to 3 at the end.
To fix this, unset($value); after your first foreach.
The thing here is that you have to unset the value when you pass it by reference:
foreach ($array as &$value) {
ScoreWords($value);
}
unset($value); // break the reference with the last element
Warning Reference of a $value and the last array element remain even
after the foreach loop. It is recommended to destroy it by unset().
Foreach reference
I have some unexpected behavior with references:
foreach ($this->data as &$row)
{
$row['h'] = 1;
}
foreach ($this->data as $id => $row)
{
... in some cases $row[$id] = $row;
}
The result is that the last element of the array is replaced with the second to last element of the array. It is fixed with the following code:
foreach ($this->data as $key => $row)
{
$this->data[$key]['h'] = 1;
}
Unfortunately, I don't have more time to spend on this. Maybe it is an error with PHP (PHP 5.5.9-1ubuntu4) or something I don't know about references?
There is a perfectly logical explanation and this is not a bug!
PHP 5 introduces the possibility of modifying the contents of the array directly by assigning the value of each element to the iterated variable by reference rather than by value. Consider this code, for example:
$a = array (’zero’,’one’,’two’);
foreach ($a as &$v) {
}
foreach ($a as $v) {
}
print_r ($a);
It would be natural to think that, since this little script does nothing to the array, it will not affect its contents... but that’s not the case! In fact, the script provides the following output:
Array
(
[0] => zero
[1] => one
[2] => one
)
As you can see, the array has been changed, and the last key now contains the value ’one’. How is that possible? The first foreach loop does not make any change to the array, just as we would expect. However, it does cause $v to be assigned a reference to each of $a’s elements, so that, by the time the loop is over, $v is, in fact, a reference to $a[2].
As soon as the second loop starts, $v is now assigned the value of each element. However, $v is already a reference to $a[2]; therefore, any value assigned to it will be copied automatically into the last element of the arrays! Thus, during the first iteration, $a[2] will become zero, then one, and then one again, being effectively copied on to itself. To solve this problem, you should always unset the variables you use in your by-reference foreach loops—or, better yet, avoid using the former altogether.
When looping over an array by reference, you need to manually let go of the reference at the end of your for loop to avoid weird behaviors like this one. So your first foreach should be:
foreach ($this->data as &$row)
{
.... code ....
}
unset($row);
In this case, unset is only destroying the reference, not the contents referenced by $row.
See the warning in the PHP foreach documentation
The following code which is displaying a value from within an array is presenting different results when wrapped in a foreach() as opposed to when I use a simple for-loop. The for-loop is presenting the correct data but the foreach() appears to be amending the input array with every iteration.
$arr = array_merge($arr1, $arr2);
for ($x = 0; $x < count($arr); $x++) {
echo $arr[90]['circread_value'];
}
foreach ($arr as $unused) {
echo $arr[90]['circread_value'];
}
The output from the for-loop is the same value over and over again (as expected):
1382429.00
1382429.00
1382429.00
1382429.00
...
The output from the foreach() shows that the 91st element in the array is changing with each iteration:
56256.00
45652.00
50726.00
317896.00
...
How can this be?
Note: I know the code above is contrived (obviously within the foreach() I'm actually wanting to do further processing and refer to each element of the array that I'm iterating through, not just look at the 91st element.) I have simply pared back the code to something simple (as part of my debugging, as much as for posting here!)
Some further information:
$arr has been created by array_merge'ing two 91-element arrays to create a 182 element array.
The behaviour I see is only happening for the 91st element - if I echo out $arr[89]['circread_value'] or $arr[91]['circread_value'], I get consistent values from the foreach() too.
The (seemingly random) values that I see in the foreach() are actually values from other elements in the array (the array as it looks prior to beginning the foreach)
The input arrays ($arr1 and $arr2) can be found here: http://pastebin.com/wQN8XXu2
Thanks for any insight. Don't hesitate to ask for further information.
foreach modifies the internal array pointer, for doesn't, because it expects you to supply an integer offset, as evidenced here http://us3.php.net/manual/en/control-structures.foreach.php
"As foreach relies on the internal array pointer changing it within the loop may lead to unexpected behavior."
There is something wrong with your code brother.
I've just tested this:
$arr = array(
0 => array('blah' => 123.42),
1 => array('blah' => 5488.87),
90 => array('blah' => 669874.923)
);
for ($x = 0; $x < count($arr); $x++) {
echo $arr[90]['blah'] . PHP_EOL;
}
foreach ($arr as $unused) {
echo $arr[90]['blah'] . PHP_EOL;
}
And it outputs fine:
669874.923
669874.923
669874.923
669874.923
669874.923
669874.923
Do you mind showing us a little more of your code?
Problem found. That 91st element of the array was actually a reference to the array. The foreach was then using the same variable name (reference to the same array - named $unused in the example snippet I gave in the question) so both were looking at the same array.
We noticed the "&" in the array dump after posting it on here for you guys, so StackOverflow has helped our debug process... thanks for everyone's input.
I'm changing the value in a multi-dimensional array and it's not staying outside of the foreach loop that's being used to traverse it.
My array initially looks something like this:
Array
{
[0] => Array
{
[name] => Bob
[age] => 33
[state] => CA
[visited] => 0
}
...
}
My PHP gets into it by going:
foreach ($people as $person){
echo $person['name']
....
logic for the visited variable
...
$person['visited'] = $calculated_visit_value;
}
If I
print_r($person)
at the end (but inside) of the foreach loop everything looks good, the value for visited is set. However, if I print_r($people) outside of the loop, $person['visited'] is not set. I don't know what I'm doing wrong.
Help is appreciated.
You are creating a new variable called $person from within that for loop and your array will never see the scope of that new variable.
You can try passing it by reference, like so:
foreach ($people as &$person){
echo $person['name'];
....
logic for the visited variable
...
$person['visited'] = $calculated_visit_value;
}
From foreach's documentation:
Unless the array is referenced, foreach operates on a copy of the
specified array and not the array itself. foreach has some side
effects on the array pointer. Don't rely on the array pointer during
or after the foreach without resetting it.
What this means is that your $person variable is a copy of what was in the array, similar in effect to this code (note that this code is for understanding only and wrong on many levels, in reality you would use the reset(), current() and next() function to loop properly over your array, see here):
for ($i = 0; $i < count($people); $i++) {
$person = $people[$i];
// code inside your foreach ...
}
So if you change the content of $person, you don't actually modify what's inside the $people array
To solve that, you can either use a referenced foreach:
foreach ($people as &$person) { // note the &
$person = $calculated_visit_value; // $person is now a reference to the original value inside $people and thus this will work as intended
}
Note that the refence is not cleared when the foreach loop ends, so at the end of it $person is still a reference to the last element of $people.
If you don't know what references are, please refer to the documentation for more info.
Or use the key to access the original array:
foreach ($people as $person_index => $person) {
$people[$person_index] = $calculated_visit_value;
}
For your information, you can use the two together
foreach ($people as $person_index => &$person { ...
The $person array is generated on each iteration, so setting that value would be overwritten on the next go through anyway.
But even so, that array only exists during the loop. You should create another array before the loop and put your values into that array during the loop.
As it has been told, "you are creating a new variable called $person from within that for loop and your array will never see the scope of that new variable."
I find this solution more robust :
foreach ($people as $key => $person)
{
echo $person['name'];
//logic for the visited variable
$people[$key]['visited']=$calculated_visit_value;
}
Recently I experienced this weird problem:
while(list($key, $value) = each($array))
was not listing all array values, where replacing it with...
foreach($array as $key => $value)
...worked perfectly.
And, I'm curious now.. what is the difference between those two?
Had you previously traversed the array? each() remembers its position in the array, so if you don't reset() it you can miss items.
reset($array);
while(list($key, $value) = each($array))
For what it's worth this method of array traversal is ancient and has been superseded by the more idiomatic foreach. I wouldn't use it unless you specifically want to take advantage of its one-item-at-a-time nature.
array each ( array &$array )
Return the current key and value pair from an array and advance the array cursor.
After each() has executed, the array cursor will be left on the next element of the array, or past the last element if it hits the end of the array. You have to use reset() if you want to traverse the array again using each.
(Source: PHP Manual)
Well, one difference is that each() will only work on arrays (well only work right). foreach will work on any object that implements the traversable interface (Which of course includes the built in array type).
There may be a micro-optimization in the foreach. Basically, foreach is equivilant to the following:
$array->rewind();
while ($array->valid()) {
$key = $array->key();
$value = $array->current();
// Do your code here
$array->next();
}
Whereas each basically does the following:
$return = $array->valid() ? array($array->key(), $array->current()) : false;
$array->next();
return $return;
So three lines are the same for both. They are both very similar. There may be some micro-optimizations in that each doesn't need to worry about the traversable interface... But that's going to be minor at best. But it's also going to be offset by doing the boolean cast and check in php code vs foreach's compiled C... Not to mention that in your while/each code, you're calling two language constructs and one function, whereas with foreach it's a single language construct...
Not to mention that foreach is MUCH more readable IMHO... So easier to read, and more flexible means that -to me- foreach is the clear winner. (that's not to say that each doesn't have its uses, but personally I've never needed it)...
Warning! Foreach creates a copy of the array so you cannot modify it while foreach is iterating over it. each() still has a purpose and can be very useful if you are doing live edits to an array while looping over it's elements and indexes.
// Foreach creates a copy
$array = [
"foo" => ['bar', 'baz'],
"bar" => ['foo'],
"baz" => ['bar'],
"batz" => ['end']
];
// while(list($i, $value) = each($array)) { // Try this next
foreach($array as $i => $value) {
print $i . "\n";
foreach($value as $index) {
unset($array[$index]);
}
}
print_r($array); // array('baz' => ['end'])
Both foreach and while will finish their loops and the array "$array" will be changed. However, the foreach loop didn't change while it was looping - so it still iterated over every element even though we had deleted them.
Update: This answer is not a mistake.
I thought this answer was pretty straight forward but it appears the majority of users here aren't able to appreciate the specific details I mention here.
Developers that have built applications using libdom (like removing elements) or other intensive map/list/dict filtering can attest to the importance of what I said here.
If you do not understand this answer it will bite you some day.
If you passed each an object to iterate over, the PHP manual warns that it may have unexpected results.
What exactly is in $array