33

This question already has an answer here:

Here is a simple loop

$list = array("A", "B", "C","D");
foreach ($list as $var) {
    print(current($list));
}

Output (demo)

 BBBB   // Output for 5.2.4 - 5.5.0alpha4
 BCD    // Output for 4.4.1
 AAAA   // Output for 4.3.0 - 4.4.0, 4.4.2 - 5.2.3

Question :

  • Can someone please explain whats going on ?
  • Why am i not getting ABCD
  • Even if a copy of the array was made by foreach i should be getting AAAA but not getting that in the current PHP stable version

Note* I know i can simply use print $var but the from PHP DOC

current — Return the current element in an array The current() function simply returns the value of the array element that's currently being pointed to by the internal pointer. It does not move the pointer in any way. If the internal pointer points beyond the end of the elements list or the array is empty, current() returns FALSE.

Update 1 - New Observation

Thanks to Daniel Figueroa : Just by wrapping current in a function you get different result

foreach ( $list as $var ) {
    print(item($list));
}

function item($list) {
    return current($list);
}

Output ( Demo )

 BCDA   // What the hell 

Question :

  • Why not getting "BBBB" ?
  • How does wrapping current in a function affect foreach output ?
  • Where did the extra "A" Come from ?

Update 2

$list = array("A","B","C","D");
item2($list);
function item2($list) {
    foreach ( $list as $var ) {
        print(current($list));
    }
}

Output ( See Demo )

AAAA // No longer BBBB when using a function

Question :

  • What is the different running a loop in a function and running it outside a function because you get AAAA outside and BBBB in a function in most PHP version


8 답변


18

Why does it start with B?

Since 5.2 foreach (reliably) advances the array pointer before the loop body starts. See also the FE_RESET opcode.

$list = array("A", "B", "C","D");
foreach ($list as $var) {
    break;
}
var_dump(current($list));

Output:

B

This may have something to with how the ZEND_OP_DATA pseudo opcode works (which isn't really documented).

Why does current() keep giving the same value?

Before the loop starts, foreach creates an internal reference to the array that you're looping over. Once inside the loop, whenever the array variable is modified or passed by reference, the internal reference is disassociated from the variable by making a copy of the array structure (but not the elements). This copied value retains the array pointer (which had earlier been modified by the loop initialization).

This behaviour is also exhibited with a more destructive unset() operation:

$list = array('A', 'B', 'C', 'D');
foreach ($list as $key => $val) {
  echo $val;
  unset($list[1], $list[2], $list[3]);
}
echo "\n", print_r($list, true), "\n";

Output:

ABCD
Array
(
    [0] => A
)

Passing loop variable to a function

This is another interesting scenario:

$list = array('A', 'B', 'C', 'D');
function itm($arr) 
{
    return current($arr);
}

foreach ($list as $item) {
    print itm($list);
}
var_dump(current($list));

Output:

BCDA
bool(false)

This time, the array is passed by value and thus its array structure is copied (not the elements) into the function's $arr parameter. Unlike the previous example, there's no disassociation between the loop's internal reference and the $list symbol because the copy takes place in the function scope.

What about the last "A"?

This is by far the most mystifying behaviour of foreach and can only be witnessed under these circumstances. In the last loop iteration, the array pointer is seemingly rewound to the first item; seemingly because at the end of the loop it obviously points beyond the end of the elements (as you can see from the last line of the output).

This may have something to do with the SWITCH_FREE opcode that's executed at the end of a foreach.

So why does placing foreach in a function make it different?

Observe the following code:

function item2($arr) 
{
    foreach ($arr as $var) {
        print(current($arr));
    }
    var_dump(current($arr));
}
$list = array("A","B","C","D");
item2($list);

Output:

AAAA
string(1) "A"

In this case, the internal reference of the foreach is initialized with a copy of the array (because it has a refcount > 1) and thus creates an immediate disassociation from the $arr symbol.

Can it get worse?

Of course! You can get even whackier results when you start using references or nest multiple foreach loops on the same variable.

So how can I get consistent results?

Use Iterators or don't rely on getting a consistent value from referencing the array variable during a foreach operation.


  • +Nice but how does it explain stackoverflow.com/a/14849560/1226894 - Baba
  • What i don't understand is why the rewind came from ? does it mean functions rewinds array ??? or return statement has effect on arrays - Baba
  • Do you know runing the loop in a function gives 3 different result -3v4l.org/1aUpd but Iterators gives same result - 3v4l.org/ViCNn - Baba
  • @Baba Yeah, I noticed that as well; iterators are so much more stable :) - Ja͢ck
  • But they can not be used with references ... :( - Baba

3

FROM PHP.net

The current() function simply returns the value of the array element that's currently being pointed to by the internal pointer. It does not move the pointer in any way

then: use next()

$list = array("A", "B", "C","D");
foreach ($list as $var) {
    print(current($list));
    next($list);
}

NOTE: the first element will not be printed because foreach moved the pointer to second element of the array :)

This example will explain the full behaviour:

$list = array("A", "B", "C","D");
foreach ($list as $var) {
   if(!isset($a)) reset($list); $a = 'isset';
   print(current($list));
   next($list);
}

out put is ABCD

Please also note that:

As foreach relies on the internal array pointer changing it within the loop 
may lead to unexpected behavior.

foreach


EDIT: I want to share also my new confusing finding!!!

Example1:

$list = array("A", "B", "C","D");
$list_copy = $list;
foreach ($list as $key => $val) {
  current($list_copy);
  echo current($list);
  //next($list);
}

OUTPUT: AAAA

Example2:

$list = array("A", "B", "C","D");
$list_copy = $list;
foreach ($list as $key => $val) {
  current($list_copy);
  echo current($list);
  next($list);
}

OUTPUT: ABCD

When calling current() function inside foreach even for another array it will affect the foreach behavior...

Example3:

$list = array("A", "B", "C","D");

$refcopy = &$list;

foreach ($list as $key => $val) {
  if(!isset($a)) { $a = 'isset'; reset($list); }
  echo current($list);
  next($list);
}

OUTPUT: ACD (WOW! B is missing)

Example: 4

$list = array("A", "B", "C","D");

$refcopy = &$list;

foreach ($list as $key => $val) {
  echo current($list);
  next($list);
}

OUTPUT: BCD

It can't be decided exactly what will happen inside foreach loop!!!


2

Well I have no real clue as to why this is, but i suspect it might have something to do with how the assignment is evaluated/processed. For fun I tried this and it resulted in another incorrect behaviour:

$arr = array('A', 'B', 'C', 'D');
function itm($val) {
    return current($val);
}

foreach ($arr as $item) {
    print itm($arr);
}

Result: BCDA

So my guess is that whats happening here is that the function call forces the evaluation of current to happen in a ~correct manner. Also the reason for me getting BCDA instead of ABCD is probably because the internal pointer at first is incremented (pointing at B) and then in the en it is reset back to point at A.

It might be worth noting this line in the PHP doc:

Note that the assignment copies the original variable to the new one (assignment by value), so changes to one will not affect the other. This may also have relevance if you need to copy something like a large array inside a tight loop.

I guess this doesn't really count as an answer but I liked your question and wanted to contribute a little.


  • Nice this is getting complex .... nice observation - Baba
  • Indeed, because the soft-copy argument is kind of void now I think. - Daniel Figueroa
  • Would add your observation to the question .. this is key ... observation - Baba
  • @DanielFigueroa I think so too. I've just tried to pass $arr by reference and it outputs BBBB. codepad.viper-7.com/6MxdFr - Leri

1

Even if a copy of the array was made by foreach i should be getting AAAA but not getting that in the current PHP stable version

Since I found no answer to this question here, I'll (try to) explain.

Before the first iteration of foreach $list is not actually copied. Only reference counting of $list will be increased to 2. So on the first iteration: first value of $list will be copied in $var, pointer will move to the second element of $list and actual copy of $list will be made. So when you call current pointer points to second element but on the second and farther iterations it's never modified because actual copy of $list exists so current always will output the second element.

Edit:

I've played with debug_zval_dump to understand this really very unexpected behavior with:

<pre>

<?php


$list = array("A", "B", "C","D");

echo '<h1>Ref count before entering foreach:</h1><br>';
debug_zval_dump($list); echo '<br><br>';

$i = 0;
echo '<h1>Ref count in foreach:</h1><br>';
foreach ($list as $var) {
    $i++;
    echo '<b>Iteration #'.$i.':</b> ';
    debug_zval_dump($list);
    echo '<br>';
}

$list = array("A", "B", "C","D"); //re-assign array to avoid confusion

echo '<h1>Ref count before entering foreach that calls method "item" and passes array by value:</h1><br>';
debug_zval_dump($list);
$i = 0;
echo '<h1>Ref count in foreach that calls method "item" and passes array by value:</h1><br>';
foreach ( $list as $var ) {
    $i++;
    item($list, $i);
}

function item($list, $i) {
    echo '<b>Iteration #'.$i.':</b> ';
    debug_zval_dump($list);
}

$list = array("A", "B", "C","D"); //re-assign array to avoid confusion

echo '<h1>Ref count before entering foreach that calls method "item" and passes array by reference:</h1><br>';
debug_zval_dump($list);
$i = 0;
echo '<h1>Ref count in foreach that calls method "item" and passes array by reference:</h1><br>';
foreach ( $list as $var ) {
    $i++;
    itemWithRef($list, $i);
}

function itemWithRef(&$list, $i) {
    echo '<b>Iteration #'.$i.':</b> ';
    debug_zval_dump($list);
}

And got the following output:

Ref count before entering foreach:
array(4) refcount(2){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(1) }

Ref count in foreach:
Iteration #1: array(4) refcount(3){ [0]=> string(1) "A" refcount(2) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(1) }
Iteration #2: array(4) refcount(3){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(2) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(1) }
Iteration #3: array(4) refcount(3){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(2) [3]=> string(1) "D" refcount(1) }
Iteration #4: array(4) refcount(3){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(2) }
Ref count before entering foreach that calls method "item" and passes array by value:
array(4) refcount(2){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(1) } Ref count in foreach that calls method "item" and passes array by value:
Iteration #1: array(4) refcount(5){ [0]=> string(1) "A" refcount(2) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(1) } Iteration #2: array(4) refcount(5){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(2) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(1) } Iteration #3: array(4) refcount(5){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(2) [3]=> string(1) "D" refcount(1) } Iteration #4: array(4) refcount(5){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(2) } Ref count before entering foreach that calls method "item" and passes array by reference:
array(4) refcount(2){ [0]=> string(1) "A" refcount(1) [1]=> string(1) "B" refcount(1) [2]=> string(1) "C" refcount(1) [3]=> string(1) "D" refcount(1) } Ref count in foreach that calls method "item" and passes array by reference:
Iteration #1: array(4) refcount(1){ [0]=> string(1) "A" refcount(4) [1]=> string(1) "B" refcount(3) [2]=> string(1) "C" refcount(3) [3]=> string(1) "D" refcount(3) } Iteration #2: array(4) refcount(1){ [0]=> string(1) "A" refcount(3) [1]=> string(1) "B" refcount(4) [2]=> string(1) "C" refcount(3) [3]=> string(1) "D" refcount(3) } Iteration #3: array(4) refcount(1){ [0]=> string(1) "A" refcount(3) [1]=> string(1) "B" refcount(3) [2]=> string(1) "C" refcount(4) [3]=> string(1) "D" refcount(3) } Iteration #4: array(4) refcount(1){ [0]=> string(1) "A" refcount(3) [1]=> string(1) "B" refcount(3) [2]=> string(1) "C" refcount(3) [3]=> string(1) "D" refcount(4) }

The output is little bit confusing.

In first example foreach has created internal copy of $list so reference count was 2 (4 in the result because debug_zval_dump adds one refCount). In the second example (pass by value) refCount increased to 3, because $list was copied for function. In the third example count kept to 1 because $list was passed by value. I need some time to realize why. If you get the point out of this result share.

All I can say is that when we passed array by value foreach was passing array that was iterating, but when passed by reference it took the original $list. The question is: why was foreach passing that array?


  • + Nice but you need to see stackoverflow.com/a/14849560/1226894 - Baba
  • @Baba Yes, I've seen and I am thinking "wtfphp". - Leri
  • I can't believe it myself - Baba
  • Do you know runing the loop in a function gives 3 different result -3v4l.org/1aUpd but Iterators gives same result - 3v4l.org/ViCNn - Baba
  • @Baba Did not know that. After this I am much more sure that we should never use these kind of structures in real world apps. It will be huge head-ache later. Behavior is simply undefined. - Leri

1

The code you use if a lie. Even literally it might look like the same code, however the variables are not (http://3v4l.org/jainJ).

To answer your actual question, for consistent results use the right tools.

If you need a variable with an array value, assign it:

$list = array(....);

If you need to get the current value of that array, use it before the foreach:

$current = current($list);

Because inside the foreach this might be the same variable name but the value will be different (imagine, you're iterating!).

If you need the current value per each iteration, use it:

foreach ($list as $current) {
    ...
}

See $current?

Oh gosh, yeah, it was that easy. Wait I already have consistent results. Oh and it was that easy to not fool myself. Yay! ;)

For the log: Passing a variable as function parameter makes it a new variable. Even when a reference (that is explained).

When in doubt, do not use PHP references. Or not even variables: http://3v4l.org/6p5nZ


  • + lol .... nice workaround - Baba

0

Great point out. But it seems memory pointing issue with different version of php. Also current gives only current position which you have not increment(navigated) anywhere so not getting proper output. As different version of php interpreting next and starting point of array in different ways a solution to this could be a reset inside the loop with some condition. (by the way looping and then using current, next prev is not a good way as already have object in var :) what ever it is your choice) This is one way you can get it work:

<?php
$list = array("A", "B", "C","D");
$flag =0;
foreach ($list as $var) {
    if($flag==0)
    {   
        reset($list);
        $flag=1;
    }
    print(current($list));
    next($list);
}

Output is ABCD. See at http://3v4l.org/5Hm5Y


-1

$list = array("A", "B", "C","D");
foreach ($list as $var) {
    echo $var;
}

Should do it.


  • Please review the problem as stated in the question. The question is about the unusual behavior of current inside a foreach, not how to obtain a specific output. - Charles

-1

Use This you already know what's happen !

$list = array('A', 'B', 'C','D');
foreach ($list as $var) {
 var_dump(current($list));
}

may be its help you!

Linked


Related

Latest