27

Why is an empty foreach loop can change the result.

I have the following code:

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

var_dump($variable);

The result I get is:

array (size=4)
  0 => int 2
  1 => int 3
  2 => int 4
  3 => &int 5

Now, when I add an empty foreach loop like this:

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

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

var_dump($variable);

I get this :

array (size=4)
  0 => int 2
  1 => int 3
  2 => int 4
  3 => &int 4

can someone explain me why the last element doesn't change when I add the second empty loop, and why there is a & infront of the last element?

Khalid
  • 4,730
  • 5
  • 27
  • 50
  • 3
    +1 That's a funky issue you have there, interested in the explanation as-well. – Darren Jul 23 '14 at 05:51
  • The second loop seems to be causing the issue no matter what. Even if I do something useful in there, it alters the result. I'll look into it. – padarom Jul 23 '14 at 05:51
  • As [mentioned in the manual](http://php.net/manual/en/control-structures.foreach.php), you should `unset()` references after use ~ *"**Warning** Reference of a `$value` and the last array element remain even after the `foreach` loop. It is recommended to destroy it by `unset()`."* – Phil Jul 23 '14 at 05:53
  • 1
    @Phil yes , I know , but I just need logical explaination – Khalid Jul 23 '14 at 05:55
  • 1
    @Khalid Going into the second loop, `$value` is still a reference to the last item in the list. Think about that – Phil Jul 23 '14 at 05:58
  • `unset()` seems to be the solution, but doesn't explain why the second (empty) foreach loop _alters_ the values. It is still set as a reference, that's known. But how is it being altered? That's the question of the OP. – padarom Jul 23 '14 at 05:59
  • nice question. bookmarked – Parfait Jul 23 '14 at 06:02

6 Answers6

19

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);

beppe9000
  • 1,056
  • 1
  • 13
  • 28
Adelmar
  • 2,073
  • 2
  • 20
  • 20
  • 2
    Isn't this a language defect? The $value variable should be local for the foreach loop and not accessible from outside. – Nuclear Nov 11 '17 at 17:41
13

This is a name collision: the name $value introduced in the first loop exists after it and is used in the second loop. So all assignments to it are in fact assignments to the original array. What you did is easier observed in this code:

  $variable = [1,2,3,4];
  foreach ($variable  as $key => &$value) 
    $value ++;
  $value = 123; // <= here you alter the array!
  var_dump($variable);

and you will see $variable[3] as 123.

One way to avoid this is, as others said, to unset ($value) after the loop, which should be a good practice as recommended by the manual. Another way is to use another variable in the second loop:

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

which does not alter your array.

Alexander Gelbukh
  • 2,104
  • 17
  • 29
  • I wish PHP and Python had a way to declare new variables -- that is, just indicate that I expect this variable to be new, as in Perl (`my`), Java (`var`), or LaTeX (`\newcommand`). Tiny extra effort, HUGE benefit in debugging time and in losses because of incorrect programs. – Alexander Gelbukh Jul 23 '14 at 06:11
  • 2
    @AlexanderGelbukh I've always thought that myself! Luckily when PHP is written well these issues are minimized by data encapsulation and scope, but unluckily PHP is often not written well... – Adelmar Jul 23 '14 at 06:30
  • 1
    @chjohasbrouck A partial solution would be to write a function `new()` which dies if the variable is `isset`, and use it systematically. It would prevent a situation described in this question. But this does not prevent from typos: `$variable = 1; $varuable++; echo $variable;`, which prints `1` with no warning. Very stupid of the creators of such languages. – Alexander Gelbukh Jul 23 '14 at 06:41
  • @AlexanderGelbukh here the problem may not only be with a variable with the same name, as well as when copying an array. the array contains a link and a copy of the array will modify the original array. – Alex78191 Dec 07 '21 at 21:42
5

The last element of the array will remian even after the foreach loop ..So its needed to use unset function outside the loop ..That is

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


}
  unset($value);
  var_dump($variable); 

The link to the manual can be found here http://php.net/manual/en/control-structures.foreach.php

Avinash Babu
  • 6,171
  • 3
  • 21
  • 26
4

As phil stated in the comments:

As mentioned in the manual, you should unset() references after use.


$variable  = [1,2,3,4];

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

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


print_r($variable);

Will return:

Array
(
    [0] => 2
    [1] => 3
    [2] => 4
    [3] => 5
)

Example


Explanation

Taken from the foreach() manual. (See the big red box)

Reference of a $value and the last array element remain even after the foreach loop. It is recommended to destroy it by unset().

It basically means: That the referenced value &$value and the last element/item in the array, which in this case is 4 remain the same. To counter-act this issue, you'll have to unset() the value after use, otherwise it will stay in the array as its original value (if that makes sense).

You should also read this: How does PHP 'foreach' actually work?

Community
  • 1
  • 1
Darren
  • 13,050
  • 4
  • 41
  • 79
  • Might as well just unset it *after* the loop – Phil Jul 23 '14 at 05:57
  • That unset should be outside the loop – Hanky Panky Jul 23 '14 at 05:57
  • 1
    yes, you are right, but can you give me explaination, why a foreach loop can cause this problem – Khalid Jul 23 '14 at 05:58
  • @Phil & Hanky - sorry took it from the eval.in where I was testing! – Darren Jul 23 '14 at 05:59
  • @Hanky웃Panky Don't really see a problem using it inside the loop, unless you're in to micro-optimisation – Phil Jul 23 '14 at 06:00
  • @Darren sorry, but I still don't get it ... after the fisrt loop, the value of `$value` is `5` (I just tested) the second loop, doesn't do anything, obviously the last value should be `5` too, how come it is 4 – Khalid Jul 23 '14 at 06:09
4

After loop you should unset this reference using:

unset($value);

So your whole code should work like this:

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

There is no point to put unset($value); inside the loop

Explanation - after loop $value is still set to the last element of array so you can use after your loop $value = 10; (before unset) and you will see that last element of array has been changed to 10. It seems that var_dump want to help us a bit in this case and shows there is reference for last element and of course when we use unset we have desired output of var_dump.

You could also look at the following script:

<?php
  $array = [1, 2, 3, 4];

  var_dump($array);

  $x = &$array[2];

  var_dump($array);

  $x += 20;

  unset($x);

  var_dump($array);

?>

We don't use loop here and if reference is set to element of array, var_dump shows us this putting & before type of this element.

However if the above code we changed reference and set it this way $x = &$array; var_dump wouldn't show us any reference.

Also in the following code:

<?php
$x = 23;
$ref = &$x;

var_dump($x);

?>

var_dump() won't give us any hint.

Marcin Nabiałek
  • 109,655
  • 42
  • 258
  • 291
  • 2
    @Hanky웃Panky it's because we are not looking for the solution, we are looking for explaination – Khalid Jul 23 '14 at 06:03
  • @Khalid I've added explanation – Marcin Nabiałek Jul 23 '14 at 06:07
  • @MarcinNabiałek your right about assaigning a value after the first loop, the last element change, I understand why the last element of the array is a reference ... but how come the second loop makes the last element like the one before ? – Khalid Jul 23 '14 at 06:13
  • @Khalid I don't see any reason to think about it. Simply the code is wrong because you don't unset reference after first loop. There is no point for me to think why it works this way – Marcin Nabiałek Jul 23 '14 at 06:19
  • @MarcinNabiałek It helps some to know why they're supposed to follow a particular advice :) – Ja͢ck Jul 23 '14 at 06:37
3

Obligatory statement: References are evil!

Stepping through your code:

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

After the loop completes; $value is a reference to $variable[3] and thus has the value of int(4).

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

At each iteration, $variable[3] gets assigned an element of $variable[<k>] where 0 <= k < 3. At the last iteration it gets assigned to its own value which is that of the previous iteration, so it's int(4).

Unsetting $value in between the two loops resolves the situation. See also an earlier answer by me.

Community
  • 1
  • 1
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • @Khalid it's a rather unfortunate design of references ... btw, stop moving the accepted answer around, only one can be the accepted one :) – Ja͢ck Jul 23 '14 at 06:22
  • sorry, I just return it to the first one who answered right :)! sorry and thanks again for you help :) – Khalid Jul 23 '14 at 06:24