让我在前面说我知道foreach
是什么,做什么以及如何使用它。这个问题涉及到它如何在引擎盖下工作,我不想沿着“这就是你如何用foreach
循环数组“的路线得到任何答案。
很长一段时间我都认为foreach
可以和数组本身一起工作。后来我发现很多引用都提到它可以和数组的一个 * 副本 * 一起工作,从那以后我就认为这就是故事的结局。但是我最近开始讨论这个问题,经过一点实验发现这不是100%正确的。
让我来说明我的意思。对于以下测试用例,我们将使用以下数组:
$array = array(1, 2, 3, 4, 5);
字符串
Test case 1:
foreach ($array as $item) {
echo "$item\n";
$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 */
型
这清楚地表明,我们没有直接使用源数组-否则循环将永远持续下去,因为我们在循环期间不断地将元素推入数组。但为了确保情况是这样的:
Test case 2:
foreach ($array as $key => $item) {
$array[$key + 1] = $item + 2;
echo "$item\n";
}
print_r($array);
/* Output in loop: 1 2 3 4 5
$array after loop: 1 3 4 5 6 7 */
型
这支持了我们最初的结论,我们在循环期间使用源数组的副本,否则我们将在循环期间看到修改后的值。* 但是... *
如果我们在manual中查找,我们会发现以下语句:
当foreach第一次开始执行时,内部数组指针自动重置为数组的第一个元素。
对...这似乎表明foreach
依赖于源数组的数组指针。但是我们刚刚证明了我们 * 不使用源数组 *,对吗?嗯,不完全是。
Test case 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 "$item\n";
}
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)
*/
型
因此,尽管我们没有直接处理源数组,但我们直接处理源数组指针-指针在循环结束时位于数组末尾的事实表明了这一点。但这不可能是真的-如果是真的,那么test case 1将永远循环。
PHP手册还指出:
由于foreach依赖于内部数组指针,因此在循环内更改它可能会导致意外行为。
好吧,让我们来看看什么是“意外行为”(从技术上讲,任何行为都是意外的,因为我不再知道会发生什么)。
Test case 4:
foreach ($array as $key => $item) {
echo "$item\n";
each($array);
}
/* Output: 1 2 3 4 5 */
foreach ($array as $key => $item) {
echo "$item\n";
reset($array);
}
/* Output: 1 2 3 4 5 */
型
.没有什么出乎意料的,事实上,它似乎支持“副本的来源”的理论。
- 问题 *
这是怎么回事?我的C-fu不足以让我通过查看PHP源代码来提取适当的结论,如果有人能为我翻译成英语,我将不胜感激。
在我看来,foreach
使用数组的 * 副本 *,但在循环后将源数组的数组指针设置为数组的末尾。
- 这是正确的和整个故事吗?
- 如果不是,它到底在做什么?
- 在
foreach
期间使用调整数组指针的函数(each()
,reset()
等)会影响循环的结果吗?
8条答案
按热度按时间snz8szmq1#
foreach
支持对三种不同类型的值进行迭代:Traversable
对象在下面,我将尝试精确地解释迭代在不同情况下是如何工作的。到目前为止,最简单的情况是
Traversable
对象,因为对于这些foreach
本质上只是代码的语法糖沿着这些行:字符串
对于内部类,通过使用内部API避免了实际的方法调用,该内部API本质上只是在C级别上镜像
Iterator
接口。数组和普通对象的迭代要复杂得多。首先,应该注意的是,在PHP中,“数组”实际上是有序的字典,它们将按照这个顺序被遍历。(只要你没有使用像
sort
这样的东西,它就匹配插入顺序)。(其他语言的列表通常如何工作)或根本没有定义的顺序(其他语言的字典通常如何工作)。这同样适用于对象,因为对象属性可以被看作是另一个(有序的)字典,将属性名称Map到它们的值,加上一些可见性处理。在大多数情况下,对象属性实际上并不是以这种相当低效的方式存储的。但是,如果你开始迭代对象,通常使用的打包表示将被转换为真实的字典。在这一点上,普通对象的迭代变得非常类似于数组的迭代(这就是为什么我在这里没有过多讨论普通对象迭代)。
到目前为止,一切都很好。迭代字典不会太难,对吧?当你意识到数组/对象在迭代过程中可能会发生变化时,问题就开始了。有多种方式会发生这种情况:
foreach ($arr as &$v)
通过引用进行迭代,那么$arr
将变成引用,您可以在迭代过程中更改它。$ref =& $arr; foreach ($ref as $v)
在迭代过程中允许修改的问题是,你当前所在的元素被删除了。假设你使用一个指针来跟踪你当前所在的数组元素。如果这个元素现在被释放了,你就会留下一个悬空指针(通常会导致segfault)。
解决这个问题有不同的方法。PHP 5和PHP 7在这方面有很大的不同,我将在下面描述这两种行为。总结一下,PHP 5的方法相当愚蠢,导致各种奇怪的边缘情况问题,而PHP 7的方法更复杂,导致更可预测和一致的行为。
作为最后一个预备,应该注意PHP使用引用计数和写时复制来管理内存。这意味着如果你“复制”一个值,你实际上只是重用旧值并增加它的引用计数(refcount)。只有当你执行某种修改时,一个真实的复制(称为“复制”)才会完成。参阅You're being lied to了解关于这个主题的更广泛的介绍。
PHP 5
内部数组指针和HashPointer
PHP 5中的数组有一个专用的“内部数组指针”(IAP),它可以正确地支持修改:每当删除一个元素时,都会检查IAP是否指向这个元素。如果指向,则会前进到下一个元素。
虽然
foreach
确实使用了IAP,但还有一个额外的复杂性:只有一个IAP,但一个数组可以是多个foreach
循环的一部分:型
为了支持只有一个内部数组指针的两个并发循环,
foreach
执行以下技巧:在执行循环体之前,foreach
会将指向当前元素的指针及其哈希值备份到per-foreachHashPointer
中。在循环体运行之后,如果该元素仍然存在,IAP将被设置回该元素。但是如果该元素已被删除,我们将只使用IAP当前所在的任何位置。这个方案基本上可以工作,但也有很多奇怪的行为,我将在下面演示其中的一些。数组复制
IAP是数组的一个可见特性(通过
current
系列函数公开),因为在写时复制语义下,对IAP的更改会被视为修改。不幸的是,这意味着foreach
在许多情况下被迫复制它正在迭代的数组。精确的条件是:1.数组不是一个引用(is_ref=0)。如果它是一个引用,那么对它的更改 * 应该 * 传播,所以它不应该被复制。
1.数组的refcount>1。如果
refcount
为1,则该数组不是共享的,我们可以直接修改它。如果数组没有重复(is_ref=0,refcount=1),那么只有它的
refcount
会递增(*)。另外,如果使用foreach
by reference,那么(可能重复的)数组将被转换为引用。将此代码视为发生重复的示例:
型
这里,
$arr
将被复制,以防止$arr
上的IAP更改泄漏到$outerArr
。根据上述条件,该阵列不是引用(is_ref=0),用于两个地方(refcount=2)。这个需求是不幸的,是次优实现的产物(这里不需要考虑迭代期间的修改,所以我们一开始就不需要使用IAP)。(*)在这里递增
refcount
听起来无害,但违反了写时复制(COW)语义:这意味着我们要修改refcount=2数组的IAP,而COW规定修改只能在refcount=1的值上执行。这种违反会导致用户可见的行为更改(而COW通常是透明的),因为迭代数组上的IAP更改将是可观察的-但仅在数组上的第一个非IAP修改之前。相反,这三个“有效”选项是a)总是重复,b)不递增refcount
,从而允许迭代数组在循环中任意修改,或者c)根本不使用IAP(PHP 7解决方案)。职位晋升顺序
为了正确理解下面的代码示例,你必须注意最后一个实现细节。在伪代码中,循环某些数据结构的“正常”方式看起来像这样:
型
然而,
foreach
,作为一个相当特殊的 snowflake ,选择做的事情略有不同:型
也就是说,数组指针在循环体运行之前就已经向前移动了。这意味着当循环体在元素
$i
上工作时,IAP已经在元素$i+1
上了。这就是为什么在迭代过程中显示修改的代码样本总是unset
下一个元素,而不是当前元素的原因。示例:您的测试用例
上面描述的三个方面应该可以让您对
foreach
实现的特性有一个大致完整的印象,我们可以继续讨论一些示例。测试用例的行为在这一点上很容易解释:
$array
以refcount=1开始,所以它不会被foreach
复制:只有refcount
递增。当循环体随后修改数组(此时refcount=2)时,复制将在此时发生。Foreach将继续处理$array
的未修改副本。foreach
将修改$array
变量的IAP。在迭代结束时,IAP为NULL(意味着迭代已经完成),each
通过返回false
来表示。each
和reset
都是引用函数。$array
在传递给它们时有一个refcount=2
,因此必须复制它。因此,foreach
将再次在单独的数组上工作。示例:foreach中
current
的效果显示各种复制行为的一个好方法是观察
foreach
循环中current()
函数的行为。考虑以下示例:型
这里你应该知道
current()
是一个by-ref函数(实际上:prefer-ref),即使它不修改数组。它必须是为了与所有其他函数(如next
)一起使用,这些函数都是by-ref。引用传递意味着数组必须分开,因此$array
和foreach-array
将是不同的。上面也提到了1
:foreach
在运行用户代码之前 * 推进数组指针,而不是之后。因此,即使代码位于第一个元素,foreach
也已经将指针推进到第二个元素。现在我们来做一个小小的修改:
型
这里我们有is_ref=1的情况,所以数组没有被复制(就像上面一样)。但是现在它是一个引用,当传递到by-ref
current()
函数时,数组不再需要复制。因此current()
和foreach
在同一个数组上工作。但是,由于foreach
前进指针的方式,你仍然可以看到off-by-one行为。在执行by-ref迭代时,您将获得相同的行为:
型
这里重要的部分是,当通过引用迭代时,foreach将使
$array
成为is_ref=1,所以基本上你有和上面一样的情况。另一个小的变化,这次我们将数组分配给另一个变量:
型
在这里,当循环开始时,
$array
的refcount是2,所以这一次我们实际上必须提前进行复制。因此,$array
和foreach使用的数组将从一开始就完全分离。这就是为什么你在循环之前获得IAP的位置(在本例中,它是在第一个位置)。示例:迭代中修改
在迭代过程中尝试解释修改是foreach所有问题的起源,因此它可以考虑这种情况下的一些示例。
考虑在同一个数组上的这些嵌套循环(其中使用by-ref迭代来确保它确实是同一个):
型
这里预期的部分是
(1, 2)
从输出中丢失,因为元素1
被删除了。可能意想不到的是外部循环在第一个元素之后停止。为什么?这背后的原因是上面描述的嵌套循环技巧:在循环体运行之前,当前IAP位置和哈希被备份到
HashPointer
中。在循环体之后,它将被恢复,但前提是元素仍然存在,否则当前IAP位置(无论它是什么)被使用。在上面的例子中,情况正是如此:外部循环的当前元素已经被移除,所以它将使用内部循环已经标记为已完成的IAP!HashPointer
备份+恢复机制的另一个结果是,通过reset()
等对IAP的更改通常不会影响foreach
。例如,以下代码的执行就像reset()
根本不存在一样:型
原因是,
reset()
临时修改IAP时,会在循环体之后恢复到当前的foreach元素。要强制reset()
对循环产生影响,必须额外删除当前元素,这样备份/恢复机制就会失败:型
但是,这些例子仍然是合理的。如果您还记得
HashPointer
还原使用指向元素的指针及其哈希来确定它是否仍然存在,那么真实的乐趣就开始了。但是:哈希有冲突,指针可以重用!这意味着,通过仔细选择数组键,我们可以让foreach
相信被删除的元素仍然存在,所以它会直接跳转到它。举个例子:型
根据前面的规则,这里我们通常应该期望输出
1, 1, 3, 4
。发生的情况是,'FYFY'
与删除的元素'EzFY'
具有相同的哈希值,并且分配器碰巧重用相同的内存位置来存储元素。因此,foreach最终直接跳转到新插入的元素,从而缩短了循环。在循环中替换迭代的实体
最后一个奇怪的情况,我想提一下,PHP允许你在循环中替换迭代的实体。所以你可以开始迭代一个数组,然后在中途用另一个数组替换它。或者开始迭代一个数组,然后用一个对象替换它:
正如你在本例中所看到的,一旦替换发生,PHP将从一开始就开始迭代另一个实体。
PHP 7
哈希表迭代器
如果你还记得的话,数组迭代的主要问题是如何在迭代中期处理元素的删除。PHP 5为此使用了一个内部数组指针(IAP),这有点不理想,因为一个数组指针必须被拉伸以支持多个同时的foreach循环 * 和 * 与
reset()
等的交互。PHP 7使用了不同的方法,也就是说,它支持创建任意数量的外部安全的哈希表迭代器。这些迭代器必须在数组中注册,从这一点开始,它们具有与IAP相同的语义:如果数组元素被删除,所有指向该元素的哈希表迭代器将前进到下一个元素。
这意味着
foreach
将不再使用IAP *。foreach
循环将绝对不会影响current()
等的结果,并且其自身的行为永远不会受到reset()
等函数的影响。数组复制
PHP 5和PHP 7之间的另一个重要变化与数组复制有关。如果数组在
foreach
循环中被修改,此时将发生复制(根据写时复制),并且foreach
将继续在旧阵列上工作。在大多数情况下,这种变化是透明的,除了更好的性能之外没有其他效果。但是,有一种情况下它会导致不同的行为,即数组预先是引用的情况:
以前的引用数组的按值迭代是特殊情况。在这种情况下,不会发生重复,因此迭代期间数组的所有修改都将反映在循环中。在PHP 7中,这种特殊情况已经消失:数组的按值迭代将始终在原始元素上工作,而忽略循环期间的任何修改。
当然,这并不适用于按引用迭代。如果你按引用迭代,所有的修改都会被循环反映出来。有趣的是,普通对象的按值迭代也是如此:
这反映了对象的by-handle语义(即,即使在by-value上下文中,它们也表现得像引用)。
示例
让我们考虑几个例子,从测试用例开始:
refcounting
和复制行为在PHP 5和PHP 7之间也完全相同)。Foreach
不再使用IAP,所以each()
不受循环的影响。它在循环前后的输出是相同的。each()
和reset()
将在更改IAP之前复制阵列,而foreach
仍然使用原始阵列。(并不是说IAP更改会有影响,即使阵列是共享的。)第二组例子与
current()
在不同reference/refcounting
配置下的行为有关。这不再有意义,因为current()
完全不受循环的影响,因此其返回值始终保持不变。然而,当我们在迭代过程中考虑修改时,我们会得到一些有趣的变化。我希望你会发现新的行为更明智。第一个例子:
正如你所看到的,外层循环在第一次迭代后不再中止,原因是两个循环现在都有完全独立的哈希表迭代器,并且两个循环之间不再有任何通过共享IAP的交叉污染。
另一个奇怪的边缘情况是,当你删除和添加碰巧具有相同哈希值的元素时,你会得到奇怪的效果:
以前HashPointer恢复机制直接跳转到新元素,因为它“看起来”像是与删除的元素相同(由于哈希和指针冲突)。由于我们不再依赖元素哈希,这不再是一个问题。
polhcujo2#
在例子3中,你不修改数组。在所有其他例子中,你修改的是内容或内部数组指针。这在PHP数组中很重要,因为赋值运算符的语义。
PHP中数组的赋值操作符更像是一个懒惰的克隆。将一个变量复制到另一个包含数组的变量将克隆数组,这与大多数语言不同。然而,除非需要,否则实际的克隆不会完成。这意味着克隆只会在其中一个变量被修改时发生(写时复制)。
下面是一个例子:
字符串
回到你的测试用例,你可以很容易地想象
foreach
创建了一个引用数组的迭代器。这个引用的工作方式和我的例子中的变量$b
完全一样。但是,迭代器沿着引用只在循环过程中存在,然后它们都被丢弃了。现在你可以看到,除了3之外,在所有情况下,数组都在循环过程中被修改了,而这个额外的引用是活的。这会触发一个克隆,这就解释了这里发生的事情!下面是一篇关于这种写时复制行为的另一个副作用的优秀文章:The PHP Ternary Operator: Fast or not?
wwwo4jvm3#
使用
foreach()
时需要注意的几点:a)
foreach
在原始阵列的预期副本上工作。这意味着foreach()
将具有SHARED数据存储,直到或除非没有创建prospected copy
foreach Notes/User comments。B)什么触发了预期拷贝?预期拷贝是基于
copy-on-write
的策略创建的,也就是说,每当传递给foreach()
的数组发生变化时,就会创建原始数组的克隆。c)原始数组和
foreach()
迭代器将有DISTINCT SENTINEL VARIABLES
,也就是说,一个用于原始数组,另一个用于foreach
;参见下面的测试代码。堆栈溢出问题 * How to make sure the value is reset in a 'foreach' loop in PHP? * 解决了您的问题的情况(3,4,5)。
下面的例子显示each()和reset()不影响
foreach()
迭代器的SENTINEL
变量(for example, the current index variable)
。字符串
输出:
型
vnjpjtjt4#
PHP 7注意事项
更新这个答案,因为它已经得到了一些普及:这个答案不再适用于PHP 7。正如在“Backward incompatible changes“中解释的那样,在PHP 7中,foreach工作在数组的副本上,所以数组本身的任何更改都不会反映在foreach循环中。更多细节在链接中。
说明(引用自php.net):
第一种形式循环array_expression给出的数组。在每次迭代中,当前元素的值被赋给$value,内部数组指针前进1(因此在下一次迭代中,您将看到下一个元素)。
因此,在第一个例子中,数组中只有一个元素,当指针移动时,下一个元素不存在,因此在添加新元素后,因为它已经“决定”它作为最后一个元素。
在第二个例子中,你从两个元素开始,foreach循环不是在最后一个元素,所以它在下一次迭代中计算数组,从而意识到数组中有新元素。
我相信这都是文档中解释的On each iteration部分的结果,这可能意味着
foreach
在调用{}
中的代码之前执行所有逻辑。测试用例
如果你运行这个:
字符串
你会得到这样的输出:
这意味着它接受了修改,并通过了它,因为它被修改“及时”。但如果你这样做:
型
您将获得:
这意味着数组被修改了,但是因为我们修改它的时候
foreach
已经在数组的最后一个元素,它“决定”不再循环,即使我们添加了新元素,我们添加它“太晚了”,它没有循环通过。详细的解释可以在How does PHP 'foreach' actually work?上阅读,它解释了这种行为背后的内部机制。
ssgvzors5#
根据PHP手册提供的文档。
在每一次迭代中,当前元素的值被赋给$v,而内部
数组指针前进1(因此在下一次迭代中,您将查看下一个元素)。
就像你的第一个例子:
字符串
$array
只有一个元素,所以每次执行时,1赋值给$v
,它没有任何其他元素来移动指针但在第二个例子中:
型
$array
有两个元素,所以现在$array计算零索引并将指针移动1。对于循环的第一次迭代,添加$array['baz']=3;
作为通过引用传递。wfauudbj6#
很好的问题,因为许多开发人员,即使是有经验的开发人员,都对PHP在foreach循环中处理数组的方式感到困惑。在标准的foreach循环中,PHP会复制循环中使用的数组。循环结束后,副本会立即丢弃。这在简单的foreach循环的操作中是透明的。例如:
字符串
这将产生:
型
所以副本被创建了,但开发人员没有注意到,因为原始数组在循环中或循环结束后没有被引用。然而,当你试图修改循环中的项目时,你会发现它们在完成时没有被修改:
型
这将产生:
型
与原始版本相比的任何更改都不可能是通知,实际上与原始版本相比没有任何更改,即使您明确地为$item分配了一个值。这是因为您正在操作$item,因为它出现在正在处理的$set的副本中。您可以通过引用抓取$item来覆盖它,如下所示:
型
这将产生:
型
因此,很明显,当$item被引用操作时,对$item的更改会对原始$set的成员进行更改。通过引用使用$item也会阻止PHP创建数组副本。为了测试这一点,首先我们将展示一个快速脚本来演示副本:
型
这将产生:
型
正如示例中所示,PHP复制了$set并使用它来循环,但是当$set在循环中使用时,PHP将变量添加到原始数组中,而不是复制的数组中。基本上,PHP只使用复制的数组执行循环和$item的赋值。因此,上面的循环只执行3次,每次它都会在原始$set的末尾添加另一个值,使原始$set有6个元素,但永远不会进入无限循环。
然而,如果我们引用$item,就像我之前提到的那样,会怎么样呢?在上面的测试中添加一个字符:
型
结果是一个无限循环。请注意,这实际上是一个无限循环,你要么自己杀死脚本,要么等待你的操作系统耗尽内存。我在我的脚本中添加了下面一行,所以PHP会很快耗尽内存,我建议你做同样的事情,如果你要运行这些无限循环测试:
型
所以在前面的无限循环的例子中,我们看到了PHP为什么要创建一个数组的副本来循环的原因。当一个副本被创建并只由循环构造本身的结构使用时,数组在整个循环执行过程中保持静态,所以你永远不会遇到问题。
ev7lccsx7#
PHP foreach循环可以与
Indexed arrays
、Associative arrays
和Object public variables
一起使用。在foreach循环中,php做的第一件事是创建一个要迭代的数组副本。PHP然后迭代这个新的数组
copy
,而不是原来的数组。这在下面的例子中演示:字符串
除此之外,php还允许使用
iterated values as a reference to the original array value
。这在下面演示:型
注意:
original array indexes
不允许作为references
使用。来源:http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples
ktca8awb8#
你的问题可能会从多个Angular 来理解。PHP Foreach是一个循环,它允许你遍历数组类型。正如预期的那样,根据Marketsplash的foreach执行:
1.数组初始化;
1.开始foreach循环;
1.处理每个项目。
正如你提到的,可以使用foreach,在项目上应用
&
符号或不应用&
符号,以实际将迭代链接到原始数组,从而直接更改原始数组-考虑下面的Marketsplash示例:字符串