关于php:foreach循环中current()的意外行为

Unexpected behaviour of current() in a foreach loop

本问题已经有最佳答案,请猛点这里访问。

这是一个简单的循环

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

输出(演示)

1
2
3
 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

问题:

  • 有人能解释一下发生了什么事吗?
  • 为什么我不明白ABCD
  • 即使阵列的副本是由foreach制作的,我也应该得到AAAA,但在当前的PHP稳定版本中不能得到。

注*我知道我可以简单地使用print $var,但是来自php文档

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.

更新1-新观察

多亏了DanielFigueroa:只需在函数中包装current,就可以得到不同的结果。

1
2
3
4
5
6
7
foreach ( $list as $var ) {
    print(item($list));
}

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

输出(演示)

1
 BCDA   // What the hell

问题:

  • 为什么不买"BBBB"?
  • 函数中的包装电流如何影响foreach输出?
  • 额外的"A"是从哪里来的?

更新2

1
2
3
4
5
6
7
$list = array("A","B","C","D");
item2($list);
function item2($list) {
    foreach ( $list as $var ) {
        print(current($list));
    }
}

输出(见演示)

1
AAAA // No longer BBBB when using a function

问题:

  • 在函数中运行循环并在函数外运行循环有什么不同,因为在大多数PHP版本中,函数中都有AAAAoutside和BBBBoutside


为什么从B开始?

由于5.2 foreach(可靠地)在循环体开始之前推进数组指针。另请参见FE_RESET操作码。

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

输出:

1
B

这可能与ZEND_OP_DATA伪操作码的工作方式有关(实际上没有记录)。

为什么current()一直给出相同的值?

在循环开始之前,foreach创建了一个对您正在循环的数组的内部引用。一旦进入循环,每当数组变量被修改或通过引用传递时,通过复制数组结构(而不是元素),内部引用将与变量分离。这个复制的值保留数组指针(之前被循环初始化修改过)。

这种行为也表现为更具破坏性的unset()操作:

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

输出:

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

将循环变量传递给函数

这是另一个有趣的场景:

1
2
3
4
5
6
7
8
9
10
$list = array('A', 'B', 'C', 'D');
function itm($arr)
{
    return current($arr);
}

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

输出:

1
2
BCDA
bool(false)

这一次,数组是按值传递的,因此它的数组结构(而不是元素)被复制到函数的$arr参数中。与前面的例子不同,循环的内部引用和$list符号之间没有分离,因为复制发生在函数范围内。

最后一个"A"怎么样?

这是迄今为止foreach最令人迷惑的行为,只有在这种情况下才能看到。在最后一个循环迭代中,数组指针似乎被重绕到第一个项;这似乎是因为在循环的末尾,它明显地指向元素的末尾之外(从输出的最后一行可以看到)。

这可能与在foreach结束时执行的SWITCH_FREE操作码有关。

那么,为什么在函数中放置EDOCX1[0]会使其不同呢?

遵循以下代码:

1
2
3
4
5
6
7
8
9
function item2($arr)
{
    foreach ($arr as $var) {
        print(current($arr));
    }
    var_dump(current($arr));
}
$list = array("A","B","C","D");
item2($list);

输出:

1
2
AAAA
string(1)"A"

在这种情况下,使用数组的副本初始化foreach的内部引用(因为它的refcount>1),从而创建与$arr符号的立即解除关联。

会更糟吗?

当然!当您开始使用引用或在同一个变量上嵌套多个foreach循环时,您可以得到更为古怪的结果。

那么我怎样才能得到一致的结果呢?

使用Iterators或不依赖于在foreach操作期间引用数组变量获得一致的值。


来自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

然后:使用next()。

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

注意:第一个元素不会被打印,因为foreach将指针移动到数组的第二个元素:)

此示例将解释完整的行为:

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

输出是ABCD

请注意:

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

foreach

编辑:我还想分享我的新发现!!!!

实例1:

1
2
3
4
5
6
7
$list = array("A","B","C","D");
$list_copy = $list;
foreach ($list as $key => $val) {
  current($list_copy);
  echo current($list);
  //next($list);
}

输出:AAAA

实例2:

1
2
3
4
5
6
7
$list = array("A","B","C","D");
$list_copy = $list;
foreach ($list as $key => $val) {
  current($list_copy);
  echo current($list);
  next($list);
}

输出:ABCD

在foreach内调用current()函数时,即使是对另一个数组也会影响foreach的行为…

例3:

1
2
3
4
5
6
7
8
9
$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);
}

输出:ACD(哇!B失踪了)

例子:4

1
2
3
4
5
6
7
8
$list = array("A","B","C","D");

$refcopy = &$list;

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

输出:BCD

无法确定foreach循环中会发生什么!!!!


我不知道这是为什么,但我怀疑这可能与如何评估/处理分配有关。为了好玩,我尝试了这个方法,结果导致了另一个incorrect行为:

1
2
3
4
5
6
7
8
$arr = array('A', 'B', 'C', 'D');
function itm($val) {
    return current($val);
}

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

结果:BCDA

所以我猜这里发生的是,函数调用强制以一种正确的方式对电流进行评估。另外,我之所以得到BCDA而不是ABCD,可能是因为内部指针首先是递增的(指向b),然后在en中,它被重置回指向a。

在php文档中应该注意这一行:

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.

我想这不算是一个答案,但我喜欢你的问题,并想贡献一点。


如果是谎言,你使用的代码。即使从字面上看,它可能看起来像相同的代码,但是变量不是(http://3v4l.org/jainj)。

要回答您的实际问题,为了获得一致的结果,请使用正确的工具。

如果需要具有数组值的变量,请将其赋值为:

1
$list = array(....);

如果需要获取该数组的当前值,请在foreach之前使用它:

1
$current = current($list);

因为在foreach中,这可能是同一个变量名,但值不同(假设您正在迭代!).

如果每次迭代都需要当前值,请使用它:

1
2
3
foreach ($list as $current) {
    ...
}

$current

哦,天哪,是的,那很容易。等等,我已经有了一致的结果。哦,那是很容易不愚弄自己。哎呀!;)

对于日志:将变量作为函数参数传递会使其成为新变量。即使参考文献(已解释)。

如果有疑问,不要使用PHP引用。甚至不包括变量:http://3v4l.org/6p5nz


即使foreach生成了数组的副本,我也应该得到aaaa,但在当前的php稳定版本中不会得到它。

既然我在这里找不到这个问题的答案,我就(试着)解释一下。

foreach的第一次迭代之前,$list实际上没有被复制。只有$list的参考计数将增加到2。因此,在第一次迭代时:$list的第一个值将被复制到$var中,指针将移动到$list的第二个元素,并生成$list的实际副本。因此,当您调用current指针指向第二个元素时,但在第二次及以后的迭代中,它从未被修改过,因为存在$list的实际副本,所以current始终会输出第二个元素。

编辑:

我和debug_zval_dump玩过,以了解这种非常意外的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[cc lang="php"]

<?php


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

echo 'Ref count before entering foreach:';
debug_zval_dump($list); echo '';

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

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

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

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

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

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

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

得到以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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)
}

输出有点混乱。

在第一个示例中,foreach创建了$list的内部副本,因此引用计数为2(结果中为4,因为debug_zval_dump添加了一个refCount)。在第二个示例(传递值)中,refCount增加到3,因为$list是为函数而复制的。在第三个示例中,计数保持为1,因为$list是按值传递的。我需要一些时间来了解原因。如果你能从这个结果中得到答案,那就分享吧。

我只能说,当我们通过值foreach传递数组时,传递的是正在迭代的数组,但当通过引用传递时,它使用的是原始$list。问题是:为什么foreach要传递该数组?


很好的指出。但不同版本的PHP似乎存在内存指向问题。同样,电流只给出当前位置,而您没有在任何地方增加(导航),因此无法获得正确的输出。由于不同版本的PHP以不同的方式解释数组的下一个和起始点,因此可以在循环内部使用某种条件重置此问题的解决方案。(通过循环,然后使用current,next prev不是一个好方法,因为var中已经有了object:)您可以选择什么)这是一种方法,你可以让它工作:

1
2
3
4
5
6
7
8
9
10
11
12
<?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);
}

输出为ABCD。参见http://3v4l.org/5hm5y


用这个你已经知道发生了什么!

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

也许它能帮助你!


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

应该这样做。