php - 'foreach '如何精确工作

  显示原文与译文双语对照的内容

让我先说一下,我知道 foreach 是什么,以及如何使用它。 这个问题关注它是如何工作的引擎盖下,然后沿着"这就是使用 foreach 循环数组的方式"的线条,我不想作出任何回答。


很长一段时间我假设 foreach 使用数组本身。 然后我发现了很多的数组不够长,可以与一个 复制自己的作品,而且我认为,这点就会结束这个故事的。 但我最近讨论了这个问题,经过少许实验,发现这实际上不是 100% 。

让我来展示我的意思。 对于以下测试用例,我们将使用以下数组:


$array = array(1, 2, 3, 4, 5);

测试用例 1:


foreach ($array as $item) {
 echo"$itemn";
 $array[] = $item;
}
print_r($array);

/* Output in loop: 1 2 3 4 5
 $array after loop: 1 2 3 4 5 1 2 3 4 5 */

这清楚地表明我们不能直接使用源数组- 否则循环会永远继续,因为我们在循环中不断将项目推到数组中。 只是为了确认情况:

测试用例 2:


foreach ($array as $key => $item) {
 $array[$key + 1] = $item + 2;
 echo"$itemn";
}

print_r($array);

/* Output in loop: 1 2 3 4 5
 $array after loop: 1 3 4 5 6 7 */

这备份了我们最初的结论,我们在循环中使用源数组的一个副本,否则我们会在循环中看到修改后的值。 ,but,

如果我们查看手册,我们会找到以下语句:

当foreach第一次开始执行时,内部数组指针会自动重置为数组的第一个元素。

对。。这似乎表明 foreach 依赖于源数组的数组指针。 源数组,right,但我们刚刚证明了我们正在没有用? 不是完全的。

测试用例 3:


//Move the array pointer on one to make sure it doesn't affect the loop
var_dump(each($array));

foreach ($array as $item) {
 echo"$itemn";
}

var_dump(each($array));

/* Output
 array(4) {
 [1]=>
 int(1)
 ["value"]=>
 int(1)
 [0]=>
 int(0)
 ["key"]=>
 int(0)
 }
 1
 2
 3
 4
 5
 bool(false)
*/

所以,尽管存在一个问题,即我们不直接处理的源数组,源数组指针- 这一事实我们正在直接与在循环的末尾指针在数组的末尾的说明了这一点。 除了这不能是真的- 如果是的话,那么测试用例 1将永远循环。

PHP手册还状态:

由于foreach依赖于内部数组指针在循环内更改它可能导致意外的行为。

我们来看看"意外的行为"是什么。

测试用例 4:


foreach ($array as $key => $item) {
 echo"$itemn";
 each($array);
}

/* Output: 1 2 3 4 5 */

测试用例 5:


foreach ($array as $key => $item) {
 echo"$itemn";
 reset($array);
}

/* Output: 1 2 3 4 5 */

没有什么出乎意料的,事实上它支持"源副本"理论。


这个问题

怎么搞的? 露西 我的C-fu不够好,无法通过查看PHP源代码提取出正确的结论,如果有人能将它翻译成英文,我很感激。

在loop,之后用一个数组的副本,但在我看来,它 foreach 工作方式设置数组指针的源数组的末尾逆反射器

  • 这是正确的还是整个故事?
  • 如果没有,它到底做了什么?
  • 有没有什么情况下使用函数,这些函数在其中调整数组指针( each()reset() 等。) foreach 期间可能因素对比赛结果的影响圈?
时间:

注意:这个答案假设你对zvals在PHP中的工作有一些基本的了解,特别是你应该知道 refcount 是什么,以及 is_ref 意味着什么。

foreach 使用所有类型的traversables,换句话说,和数组,带有普通对象( 遍历可以访问属性的位置) 和 Traversable 对象( 或者定义内部 get_iterator 处理程序的对象) 。 这个答案主要集中在数组,我将在最后提到其他的。

但是,在深入讨论一些在这个上下文中重要的数组及其迭代之前:

数组迭代的背景

PHP中的数组是有序哈希表( 例如 。 哈希桶是双向链表的一部分,foreach 将根据该顺序遍历数组。

PHP内部有两种机制来遍历数组: 第一个是内部数组指针。 这里指针是 HashTable 结构的一部分,它基本上只是指向当前 hashtable Bucket的指针。 数组内部的指针会安全免受修改,换句话说,如果当前 Bucket 被移除,那么数组内部的指针将被更新为指向下一个桶中。

第二个迭代机制是一个外部数组指针,称为 HashPosition 。 这与内部数组指针基本相同,但不存储在 HashTable 本身中。 这个外部迭代机制是不安全防修改。 如果你删除 HashPosition 当前指向的bucket,那么你将留下一个悬空指针,导致一个分段错误。

因为这样的外部数组指针只能在你绝对确定的时候使用,所以在迭代期间,没有用户代码将被执行。 用户代码可以通过很多方式运行,比如 在一个错误处理程序,一个强制转换或者一个zval销毁。 这就是为什么在大多数情况下PHP必须使用内部数组指针而不是外部数组指针。 如果不是 PHP,当用户开始做奇怪的事情时就会出错。

内部数组指针的问题是它是 HashTable的一部分。 因此,当你修改它时,你正在修改 HashTable,并将它的作为数组。 因为PHP数组具有 by-value ( 而不是 by-reference ) 语义,这意味着每当你想迭代数组时都需要复制数组。

为什么复制真的是必要的一个简单例子( 而不仅仅是一些puristic的问题) 是一个嵌套迭代:


foreach ($array as $a) {
 foreach ($array as $b) {
//...
 }
}

这里你希望两个循环是独立的,而不是以某种奇怪的方式共享同一个数组指针。

这就引出了 foreach:

foreach中的数组迭代

现在你知道为什么 foreach 必须在遍历数组之前执行数组复制。 但这显然不是整个故事。 PHP是否实际执行复制取决于几个因素:

  • 如果该迭代的数组是一个引用,那么该副本将不发生,而不是只有一个 addref 也就此结束:

    
    $ref =& $array;//$array has is_ref=1 now
    foreach ($array as $val) {
    //...
    }
    
    

    为什么因为数组的任何更改都应该传播到引用,包括内部数组指针。 如果 foreach 在这种情况下进行了复制,它将破坏引用语义。

  • 如果该数组的refcount为 1,则将不执行复制操作。 refcount=1 意味着数组在其他地方没有使用,所以 foreach 可以直接使用它。 如果 refcount 大于 1,则表示数组与其他变量共享,为了避免修改,foreach 必须复制它( 除了上面提到的引用案例) 。

  • 如果数组是迭代的by-reference ( foreach ($array as &$ref) ),然后- 除了上面的复制/no-copy行为之外,数组将在以后进行引用。

这是谜团的第一个部分: 复制行为。第二部分是如何完成实际的迭代,这也有点奇怪。 像这样的模式,你知道( 这也通常用于 PHP - 除了 foreach ) 是东西"常用"迭代( 伪代码):


reset();
while (get_current_data(&data) == SUCCESS) {
 code();
 move_forward();
}

foreach 迭代看起来有点不同:


reset();
while (get_current_data(&data) == SUCCESS) {
 move_forward();
 code();
}

不同的是,move_forward() 在一个循环的末尾没有被调用,而是在开始时。 因此,当你的用户定义代码处理元素 $i 时,内部数组指针已经在元素 $i+1 处。

这种特性,foreach也是之所以在接下来 桶如果当前数组内部的指针会设置为一个是一个,而不是先前的( 就像你所期望的) 中移除。 它可以很好地与 foreach ( 但是它显然不能很好地与其他元素一起工作,并且将忽略这种情况下的数组元素) 一起工作。

对代码的影响

上面描述的行为的第一个含义是 foreach 必须复制它在许多情况下迭代的数组,( 慢速) 。 但不要害怕:我真的尝试了删除复制它的需求,除了人工基准测试之外,我真的看不到性能差异。 似乎人们没有足够的迭代:p

第二个含义是通常是,不应该有任何意义。 foreach的行为对于用户来说通常是相当透明的,只适用于它应该做的工作。 你不需要担心是否以及如何制作一个副本,以及什么时候准确地创建一个指针。

第三所涉的问题是- - 这和现在 我们正在前往你的问题有些时候你做使用才能看到一些非常奇怪和难以理解的行为。 当你试图修改foreach中的数组时发生这种情况。

在迭代期间修改数组时出现的大量edge-case行为集合可以在 PHP testsuite中找到。 在各种状况启动这一试验再增加库存数量 012013 等你可以了解foreach将 behave.

但现在你的实际示例:

  • 测试用例 1: 在这个循环之前 $arrayrefcount=1,所以它不会被复制,但是它会得到一个 addref 。 一旦你做了 $array[],你将把元素和迭代中的元素分开。

  • 测试用例 2: 在测试用例 1中同样适用。

  • 测试用例 3: 同样的故事。 在foreach循环中,你有 refcount=1,因此只有一个 addref,因此 $array的内部指针将被修改。 所以在循环结束时指针为空( 意义迭代已经完成) 。 each 通过返回 false 指明了这一点。

  • 测试用例 4和 5: eachreset 都是by-reference函数。 $array 在传递给它们时有一个 refcount=2,所以它必须分开。 所以在这里,foreach 将处理一个单独的数组。

但是那些测试用例是蹩脚的。 只在loop,该行为开始变得真正unintuitive当你使用一个函数比如


foreach ($array as $val) {
 var_dump(current($array));
}
/* Output: 2 2 2 2 2 */

在这里你也应该知道 current 是 一个by-ref函数,即使它不修改该数组。 它必须是为了配合所有其他的函数,比如 next,它们都是 by-ref 。 ( current 实际上是一个prefer-ref函数。 它也可以取一个值,但如果可以的话,会使用一个引用。引用意味着数组必须被分离和 $array,并且foreach-array将是不同的。 你得到 2 而不是 1的原因如下: foreach 在运行用户代码之前将数组指针前移,而不是在。 所以即使代码位于第一个元素,foreach已经将指针提前到第二个元素。

现在,我们尝试一个小修改:


$ref = &$array;
foreach ($array as $val) {
 var_dump(current($array));
}
/* Output: 2 3 4 5 false */

这里有 is_ref=1 案例,所以数组没有被复制。 但既然将该数组is_ref已经不再处于分离状态,当传给了 by-ref current 函数。 现在 currentforeach 在同一个数组上工作。 你仍然可以看到off-by-one行为,因为 foreach 将指针向前推进。

执行by-ref迭代时,你会得到相同的行为:


foreach ($array as &$val) {
 var_dump(current($array));
}
/* Output: 2 3 4 5 false */

这里重要的部分是,当 $array 被引用迭代时,它将使成为一个 is_ref=1,所以基本上你有同样的情况。

另一个小变化,这次我们将数组分配给另一个变量:


$foo = $array;
foreach ($array as $val) {
 var_dump(current($array));
}
/* Output: 1 1 1 1 1 */

在这里,当循环启动时,$array的refcount 2 is是,所以我们实际上必须先进行复制。 因此,$array 和foreach使用的数组将完全独立于 start 。 这就是为什么你在循环( 在这种情况下,它位于第一个位置) 之前得到内部数组指针的位置。

对象的迭代

迭代对象时,有两种情况:

对象不是 Traversable ( 或者更改为: 不指定内部 get_iterator 处理程序) 。 在这种情况下,迭代非常类似于数组。 相同的复制语义应用。 唯一的区别是,foreach将运行一些附加代码来跳过当前作用域中不可见的属性。 还有一些随机事实:

  • 对于声明的属性,PHP会优化属性 hashtable 。 如果你正在对一个对象进行迭代,但它必须重建这个 hashtable ( 增加内存使用量) 。 [Not that this should bother you, just a bit of trivia ]

  • 属性的哈希值在每次迭代时都是 refetched,换句话说,PHP会反复调用 get_properties 处理程序。 对于"普通"属性,这一点差别不大,但是如果在处理程序( 这是一些内部类经常做的事情) 中动态创建属性,那么就意味着每次都会重新计算属性表。

Traversable ) 对象是。 突然pretty much all what has been said上述排除任何减损。 在这种情况下,PHP不会进行复制,也不会做任何"在循环前提前指针"把戏。 我认为 Traversable 上的迭代行为更加可以预测,并且不需要解释:)

在循环中替换迭代实体

我还没有提到的另一个奇怪的例子是,PHP允许你在循环中替换迭代实体。 所以你可以在一个数组上开始迭代,然后用另一个数组替换。 或者开始迭代一个数组,然后用一个对象替换它:


$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];

$ref =& $arr;
foreach ($ref as $val) {
 echo"$valn";
 if ($val == 3) {
 $ref = $obj;
 }
}
/* Output: 1 2 3 6 7 8 9 10 */

就像你在这个例子中看到的那样,一旦替换发生,PHP就开始从开始迭代另一个实体。

在迭代期间更改内部数组指针

我还没有提到的( 因为它可以被用来 get 的怪异行为) 行为的最后一个细节是,当你尝试在迭代期间更改内部数组指针时,会发生什么。

它可能无法满足你的要求: 当你在循环体中调用 next 或者 prev 时,你可以看到内部数组指针被移动,但是它仍然不会改变迭代行为。 原因是 foreach 在每次迭代后将当前元素的当前位置和哈希值备份到一个 HashPointer 中。 在下一次迭代中,foreach 将检查内部位置是否改变,并尝试将它的恢复到旧元素( 基于哈希) 。

让我们看看"试用"的含义。 首先,这是一个例子,展示了如何改变内部指针不foreach行为:


$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
 var_dump($value);
 reset($array);
}
//output: 1, 2, 3, 4, 5

现在让我们取消在第一个迭代( 密钥 1 ) 上的元素:


$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
 var_dump($value);
 unset($array[1]);
 reset($array);
}
//output: 1, 1, 3, 4, 5

你可以看到,这次重置发生了( 双 1 ),因为已经删除了备份哈希的元素。

现在记住,哈希是: 哈希。换句话说,有冲突。 所以,让我们先尝试以下代码 Fragment:


$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
 unset($array['EzFY']);
 $array['FYFZ'] = 4;
 reset($array);
 var_dump($value);
}
//output: 1 1 3 4

这里操作按预期方式运行。我们删除了 EzFY 密钥( foreach当前所在的位置),因此 reset 发生。 另外,我们设置了一个额外的键,所以在迭代结束时添加了 4

但现在奇怪的是。 如果我们设置 FYFY 键而不是 FYFZ 则会发生什么? 让我们试试:


$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
 unset($array['EzFY']);
 $array['FYFY'] = 4;
 reset($array);
 var_dump($value);
}
//output: 1 4

现在循环直接转到新元素,跳过所有其他元素。 原因是 FYFY 键与 EzFY ( 实际上,数组中的所有键碰撞) 冲突。 此外的桶 FYFY 是发生在同一内存地址的桶 EzFY 只是下降。 所以对于PHP来说,它看起来仍然是相同的位置,具有相同的哈希。 所以位置是"已经还原",因此跳转到数组的末尾。

在示例 3中,你不修改数组。 在所有其他示例中,修改内容或者内部数组指针。 对于 PHP 数组,这很重要,因为赋值运算符的语义。

PHP中数组的赋值操作符更像一个惰性的克隆。 将一个变量分配给另一个包含数组的变量将克隆数组,与大多数语言不同。 但是,除非需要进行实际克隆,否则不会进行实际克隆。 这意味着只有当一个变量被修改时,克隆才会发生( copy-on-write ) 。

以下是一个示例:


$a = array(1,2,3);
$b = $a;//This is lazy cloning of $a. For the time
//being $a and $b point to the same internal
//data structure.

$a[] = 3;//Here $a changes, which triggers the actual
//cloning. From now on, $a and $b are two
//different data structures. The same would
//happen if there were a change in $b.

回到你的测试用例,你可以很容易地想到 foreach 创建了某种类型的迭代器。 这里引用与我的示例中的变量 $b 完全一样。 但是,迭代器和引用仅在循环期间才存在,它们都被丢弃。 现在你可以看到,在所有情况下,数组在循环期间被修改,而这个额外的引用是存活的。 这触发了一个克隆,这解释了这里发生了什么 !

以下是copy-on-write行为的另一个副作用: 三元运算符: 快还是不?

使用 foreach() 时需要注意的几点:

foreach ) 工作于原始数组的 prospected 。 它的意思是 foreach() 有共享数据的存储,直到或者除非不创建一个 prospected copyforeach笔记/用户评论

b ) 是什么导致一个 进行了展望副本? 基于 copy-on-write的策略创建副本,也就是说,每当一个传递到 foreach()的数组被改变时,原始数组的克隆就会被创建。

foreach() ) 原始数组和迭代器将有 DISTINCT SENTINEL VARIABLES,也就是说,一个用于原始数组,另一个用于 foreach ;参见下面的测试代码。 SPL迭代器以及数组迭代器

堆栈溢出问题 如何确保在PHP中的'foreach'循环中重设值? 是你问题的( 3,4,5 ) 。

下面的示例显示 each() 和 reset() 不影响 SENTINEL 变量 (for example, the current index variable) foreach() 迭代器的。


$array = array(1, 2, 3, 4, 5);

list($key2, $val2) = each($array);
echo"each() Original (outside): $key2 => $val2<br/>";

foreach($array as $key => $val){
 echo"foreach: $key => $val<br/>";

 list($key2,$val2) = each($array);
 echo"each() Original(inside): $key2 => $val2<br/>";

 echo"--------Iteration--------<br/>";
 if ($key == 3){
 echo"Resetting original array pointer<br/>";
 reset($array);
 }
}

list($key2, $val2) = each($array);
echo"each() Original (outside): $key2 => $val2<br/>";

输出:


each() Original (outside): 0 => 1
foreach: 0 => 1
each() Original(inside): 1 => 2
--------Iteration--------
foreach: 1 => 2
each() Original(inside): 2 => 3
--------Iteration--------
foreach: 2 => 3
each() Original(inside): 3 => 4
--------Iteration--------
foreach: 3 => 4
each() Original(inside): 4 => 5
--------Iteration--------
Resetting original array pointer
foreach: 4 => 5
each() Original(inside): 0=>1
--------Iteration--------
each() Original (outside): 1 => 2

解释( 来自 php.net的引用):

第一个窗体在array_expression给出的数组上循环。 在每次迭代中,当前元素的值分配给 $value,内部数组指针由一个( 所以在下一个迭代中,你将看到下一个元素) 提前。

因此,在你的第一个例子中,你只有一个元素在数组中,当指针会移动不存在,所以当你添加新的元素下一个元素foreach结束,因为它已经"决定",它它它作为最后一个元素。

在array,在你的第二个示例中,你首先创建两个数组,并不限于只不是到最后一个元素,这样它都会计算元素在下一次迭代中并因此意识到了有新 element.

我相信这都是的后果就是 foreach 部分的解释在这个文献中,这可能意味着每次循环时,它将调用之前完成了所有逻辑代码放在 {}

测试用例

如果你运行以下命令:


<?
 $array = Array(
 'foo' => 1,
 'bar' => 2
 );
 foreach($array as $k=>&$v) {
 $array['baz']=3;
 echo $v."";
 }
 print_r($array);
?>

你将得到以下输出:


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

这意味着它接受了修改并经过了修改,因为它被修改了"及时"。 但如果你这样做:


<?
 $array = Array(
 'foo' => 1,
 'bar' => 2
 );
 foreach($array as $k=>&$v) {
 if ($k=='bar') {
 $array['baz']=3;
 }
 echo $v."";
 }
 print_r($array);
?>

你将得到:


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

这意味着数组的修改时间,但是由于我们在改变着这些记忆时,已经是在数组的最后一个元素,它"决定"不去 foreach 循环再也,并且,即使我们加入了新的元素,我们添加这"太迟",而且并不是串通过做成。

详细的解释可以在如何读取foreach会有效这解释了这种行为背后内幕。

根据PHP手册提供的文档。

在每次迭代中,当前元素的值被分配给 $v 和内部元素
数组指针由一个( 所以在下一个迭代中,你将看到下一个元素) 推进。

按照你的第一个示例:


$array = ['foo'=>1];
foreach($array as $k=>&$v)
{
 $array['bar']=2;
 echo($v);
}

$array 只有单个元素,因此按照foreach执行,1分配给 $v,它没有任何其他元素可以移动指针

但在你的第二个示例中:


$array = ['foo'=>1, 'bar'=>2];
foreach($array as $k=>&$v)
{
 $array['baz']=3;
 echo($v);
}

$array 有两个元素,所以现在 $array 计算零索引并将指针移动一个。 对于循环的第一个迭代,添加了 $array['baz']=3; 作为参数传递。

...