PHP Pass by Reference in Foreach

PHP Pass by reference in foreach

Because on the second loop, $v is still a reference to the last array item, so it's overwritten each time.

You can see it like that:

$a = array ('zero','one','two', 'three');

foreach ($a as &$v) {

}

foreach ($a as $v) {
echo $v.'-'.$a[3].PHP_EOL;
}

As you can see, the last array item takes the current loop value: 'zero', 'one', 'two', and then it's just 'two'... : )

When is foreach with a parameter by reference dangerous?

About foreach

First of all, some (maybe obvious) clarifications about two behaviors of PHP:

  1. foreach($array as $item) will leave the variable $item untouched after the loop. If the variable is a reference, as in foreach($array as &$item), it will "point" to the last element of the array even after the loop.

  2. When a variable is a reference then the assignation, e.g. $item = 'foo'; will change whatever the reference is pointing to, not the variable ($item) itself. This is also true for a subsequent foreach($array2 as $item) which will treat $item as a reference if it has been created as such and therefore will modify whatever the reference is pointing to (the last element of the array used in the previous foreach in this case).

Obviously this is very error prone and that is why you should always unset the reference used in a foreach to ensure following writes do not modify the last element (as in example #10 of the doc for the type array).

About the function that modifies the array

It's worth noting that - as pointed out in a comment by @iainn - the behavior in your example has nothing to do with foreach. The mere existence of a reference to an element of the array will allow this element to be modified. Example:

function should_not_modify($array){
$array[0] = 'modified';
$array[1] = 'modified2';
}
$array = ['test', 'test2'];
$item = & $array[0];

should_not_modify($array);
var_dump($array);

Will output:

array(2) {
[0] =>
string(8) "modified"
[1] =>
string(5) "test2"
}

This is admittedly very suprising but explained in the PHP documentation "What References Do"

Note, however, that references inside arrays are potentially dangerous. Doing a normal (not by reference) assignment with a reference on the right side does not turn the left side into a reference, but references inside arrays are preserved in these normal assignments. This also applies to function calls where the array is passed by value. [...] In other words, the reference behavior of arrays is defined in an element-by-element basis; the reference behavior of individual elements is dissociated from the reference status of the array container.

With the following example (copy/pasted):

/* Assignment of array variables */
$arr = array(1);
$a =& $arr[0]; //$a and $arr[0] are in the same reference set
$arr2 = $arr; //not an assignment-by-reference!
$arr2[0]++;
/* $a == 2, $arr == array(2) */
/* The contents of $arr are changed even though it's not a reference! */

It's important to understand that when creating a reference, for example $a = &$b then both $a and $b are equal. $a is not pointing to $b or vice versa. $a and $b are pointing to the same place.

So when you do $item = & $array[0]; you actually make $array[0] pointing to the same place as $item. Since $item is a global variable, and references inside array are preserved, then modifying $array[0] from anywhere (even from within the function) modifies it globally.

Conclusion

Are there other cases that are dangerous, and can we build an exhaustive list of what is dangerous. Or the other way round: is it possible to describe when it is not dangerous.

I'm going to repeat the quote from the PHP doc again: "references inside arrays are potentially dangerous".

So no, it's not possible to describe when it is not dangerous, because it is never not dangerous. It's too easy to forget that $item has been created as a reference (or that a global reference as been created and not destroyed), and reuse it elsewhere in your code and corrupt the array. This has long been a topic of debate (in this bug for example), and people call it either a bug or a feature...

foreach loop and reference of &$value

At the end of the first loop, $value is pointing to the same place as $variable[3] (they are pointing to the same location in memory):

$variable  = [1,2,3,4];
foreach ($variable as $key => &$value)
$value ++;

Even as this loop is finished, $value is still a reference that's pointing to the same location in memory as $variable[3], so each time you store a value in $value, this also overwrites the value stored for $variable[3]:

foreach ($variable as $key => $value);
var_dump($variable);

With each evaluation of this foreach, both $value and $variable[3] are becoming equal to the value of the iterable item in $variable.

So in the 3rd iteration of the second loop, $value and $variable[3] become equal to 4 by reference, then during the 4th and final iteration of the second loop, nothing changes because you're passing the value of $variable[3] (which is still &$value) to $value (which is still &$value).

It's very confusing, but it's not even slightly idiosyncratic; it's the code executing exactly as it should.

More info here: PHP: Passing by Reference


To prevent this behavior it is sufficient to add an unset($value); statement after each loop where it is used. An alternative to the unset may be to enclose the foreach loop in a self calling closure, in order to force $value to be local, but the amount of additional characters needed to do that is bigger than just unsetting it:

(function($variable){
foreach ($variable as $key => &$value) $value++;
})($variable);

Objects in foreach Loop are Passed by Reference Without &

PHP paradigm is that objects (and resources) are always references, while other types (base types or arrays) are copied, so the & operator has no effect on objects (and is meaningless on resources since only "special functions" i.e. external library modules can take them as parameters), but allows to pass variables of other types by reference.

php foreach, why using pass by reference of a array is fast?

I thought that if the $v don't change [foreach($a as $v)], the real copy will not happen because of copy on write, but why it is fast when pass by reference?

The impact is not on $v but on $a, the huge array. You either pass it as value or as reference to the function. Inside the function it's then value (test1) or reference (test2).

You have two codes (code 1 and code 2).

Code 1: Is using foreach. With foreach you've got two options: iterate over a value or a reference (Example). When you iterate over a value, the iteration is done on a copy of the value. If you iterate over a reference, no copy is done.

As you use the reference in test2, it's faster. The values do not need to be copied. But in test1, you pass the array as value, the array gets copied.

Code 2: Is using for. For does nothing actually here. In both cases. You access the variable and read value from the array. That's pretty much the same regardless if it's a reference or a copy (thanks to the copy on write optimization in PHP).

You might now wonder, why there is a difference in code 2. The difference is not because of for but because of count. If you pass a reference to count PHP internally creates a copy of it because it count needs a copy, not a reference.

Read as well: Do not use PHP references by Johannes Schlüter


I've compiled a set of tests as well. But I more specifically put code into the test functions.

  • Blank - What's the difference in calling the function?
  • Count - Does count make a difference?
  • For - What happens with foronly (not count)?
  • Foreach - Just foreach - even breaking on first element.

Every test is in two versions, one called _copy (passing the array as copy into the function) and one called _ref (passing the array as reference).

It's not always that these micro-benchmarks tell you the truth, but if you're able to isolate specific points, you can quite well do an educated guess, for example that not for but count had the impact:

function blank_copy($a){
}
function blank_ref(&$a){
}
function foreach_copy($a){
foreach($a as $v) break;
}
function foreach_ref(&$a){
foreach($a as $v) break;
}
function count_copy($a){
$cnt = count($a);
}
function count_ref(&$a){
$cnt = count($a);
}
function for_copy($a){
for($i=0;$i<100000;$i++)
$a[$i];
}
function for_ref(&$a){
for($i=0;$i<100000;$i++)
$a[$i];
}

$tests = array('blank_copy', 'blank_ref', 'foreach_copy', 'foreach_ref', 'count_copy', 'count_ref', 'for_copy', 'for_ref');


$x = array_fill(0, 100000, 'xxxxx');
$count = count($x);
$runs = 10;

ob_start();

for($i=0;$i<10;$i++)
{
shuffle($tests);
foreach($tests as $test)
{
$begin = microtime(true);
for($r=0;$r<$runs;$r++)
$test($x);
$end = microtime(true);
$result = $end - $begin;
printf("* %'.-16s: %f\n", $test, $result);
}
}

$buffer = explode("\n", ob_get_clean());
sort($buffer);
echo implode("\n", $buffer);

Output:

* blank_copy......: 0.000011
* blank_copy......: 0.000011
* blank_copy......: 0.000012
* blank_copy......: 0.000012
* blank_copy......: 0.000012
* blank_copy......: 0.000015
* blank_copy......: 0.000015
* blank_copy......: 0.000015
* blank_copy......: 0.000015
* blank_copy......: 0.000020
* blank_ref.......: 0.000012
* blank_ref.......: 0.000012
* blank_ref.......: 0.000014
* blank_ref.......: 0.000014
* blank_ref.......: 0.000014
* blank_ref.......: 0.000014
* blank_ref.......: 0.000015
* blank_ref.......: 0.000015
* blank_ref.......: 0.000015
* blank_ref.......: 0.000015
* count_copy......: 0.000020
* count_copy......: 0.000022
* count_copy......: 0.000022
* count_copy......: 0.000023
* count_copy......: 0.000024
* count_copy......: 0.000025
* count_copy......: 0.000025
* count_copy......: 0.000025
* count_copy......: 0.000026
* count_copy......: 0.000031
* count_ref.......: 0.113634
* count_ref.......: 0.114165
* count_ref.......: 0.114390
* count_ref.......: 0.114878
* count_ref.......: 0.114923
* count_ref.......: 0.115106
* count_ref.......: 0.116698
* count_ref.......: 0.118077
* count_ref.......: 0.118197
* count_ref.......: 0.123201
* for_copy........: 0.190837
* for_copy........: 0.191883
* for_copy........: 0.193080
* for_copy........: 0.194947
* for_copy........: 0.195045
* for_copy........: 0.195944
* for_copy........: 0.198314
* for_copy........: 0.198878
* for_copy........: 0.200016
* for_copy........: 0.227953
* for_ref.........: 0.191918
* for_ref.........: 0.194227
* for_ref.........: 0.195952
* for_ref.........: 0.196045
* for_ref.........: 0.197392
* for_ref.........: 0.197730
* for_ref.........: 0.201936
* for_ref.........: 0.207102
* for_ref.........: 0.208017
* for_ref.........: 0.217156
* foreach_copy....: 0.111968
* foreach_copy....: 0.113224
* foreach_copy....: 0.113574
* foreach_copy....: 0.113575
* foreach_copy....: 0.113879
* foreach_copy....: 0.113959
* foreach_copy....: 0.114194
* foreach_copy....: 0.114450
* foreach_copy....: 0.114610
* foreach_copy....: 0.118020
* foreach_ref.....: 0.000015
* foreach_ref.....: 0.000016
* foreach_ref.....: 0.000016
* foreach_ref.....: 0.000016
* foreach_ref.....: 0.000018
* foreach_ref.....: 0.000019
* foreach_ref.....: 0.000019
* foreach_ref.....: 0.000019
* foreach_ref.....: 0.000019
* foreach_ref.....: 0.000020

PHP Foreach Pass by Reference: Last Element Duplicating? (Bug?)

After the first foreach loop, $item is still a reference to some value which is also being used by $arr[2]. So each foreach call in the second loop, which does not call by reference, replaces that value, and thus $arr[2], with the new value.

So loop 1, the value and $arr[2] become $arr[0], which is 'foo'.

Loop 2, the value and $arr[2] become $arr[1], which is 'bar'.

Loop 3, the value and $arr[2] become $arr[2], which is 'bar' (because of loop 2).

The value 'baz' is actually lost at the first call of the second foreach loop.

Debugging the Output

For each iteration of the loop, we'll echo the value of $item as well as recursively print the array $arr.

When the first loop is run through, we see this output:

foo
Array ( [0] => foo [1] => bar [2] => baz )

bar
Array ( [0] => foo [1] => bar [2] => baz )

baz
Array ( [0] => foo [1] => bar [2] => baz )

At the end of the loop, $item is still pointing to the same place as $arr[2].

When the second loop is run through, we see this output:

foo
Array ( [0] => foo [1] => bar [2] => foo )

bar
Array ( [0] => foo [1] => bar [2] => bar )

bar
Array ( [0] => foo [1] => bar [2] => bar )

You'll notice how each time array put a new value into $item, it also updated $arr[3] with that same value, since they are both still pointing to the same location. When the loop gets to the third value of the array, it will contain the value bar because it was just set by the previous iteration of that loop.

Is it a bug?

No. This is the behavior of a referenced item, and not a bug. It would be similar to running something like:

for ($i = 0; $i < count($arr); $i++) { $item = $arr[$i]; }

A foreach loop isn't special in nature in which it can ignore referenced items. It's simply setting that variable to the new value each time like you would outside of a loop.

Alternative to foreach pass by reference

You can use array_walk():

array_walk($x, function(&$y) {
/* ... */
});

This makes the reference $y local to the scope of the callback function so that unsetting is handled automatically.



Related Topics



Leave a reply



Submit