让我先说一下,我知道foreach是什么、做什么以及如何使用它。这个问题涉及到它在引擎盖下是如何工作的,我不想得到任何类似“这就是如何使用foreach循环数组”的答案
很长一段时间以来,我一直认为foreach可以处理数组本身。然后我发现了很多关于它与数组的副本一起工作这一事实的参考资料,我认为这就是故事的结尾。但我最近开始讨论这个问题,经过一点实验后发现,事实上这并不是100%正确的
让我来说明我的意思。对于以下测试用例,我们将使用以下阵列:
$array=array(1,2,3,4,5);
测试用例1:
foreach($item形式的数组){
回显“$item\n”;
$array[]=$item;
}
打印(数组);
/*回路中的输出:1 2 3 4 5
循环后的$array:12345 12345*/
这清楚地表明我们并没有直接使用源数组-否则循环将永远继续,因为我们在循环过程中不断地将项目推送到数组上。但为了确保情况确实如此:
测试用例2:
foreach($key数组=>;$item){
$array[$key+1]=$item+2;
回显“$item\n”;
}
打印(数组);
/*回路中的输出:1 2 3 4 5
循环后的$数组:1 3 4 5 6 7*/
这支持了我们最初的结论,我们在循环期间使用源数组的副本,否则我们将在循环期间看到修改后的值<但是…
如果我们查看手册,我们会发现以下陈述:
当foreach第一次开始执行时,内部数组指针将自动重置为数组的第一个元素
对。。。这似乎表明foreach依赖于源数组的数组指针。但是我们刚刚证明了我们没有使用源阵列,对吗?嗯,不完全是这样
测试用例3:
//将数组指针移到一个上,以确保它不会影响循环
var_转储(每个($array));
foreach($数组作为$项){
回显“$item\n”;
}
var_转储(每个($array));
/*输出
阵列(4){
[1] =>;
int(1)
[“值”]=>;
int(1)
[0]=>;
int(0)
[“键”]=>;
int(0)
}
1.
2.
3.
4.
5.
布尔(假)
*/
因此,尽管我们没有直接使用源数组,但我们直接使用源数组指针-指针位于循环末尾的数组末尾的事实表明了这一点。除非这不可能是真的,否则测试用例1将永远循环
PHP手册还规定:
因为foreach依赖于内部数组指针,所以在循环中更改它可能会导致意外行为
好吧,让我们来看看“意外行为”是什么(从技术上讲,任何行为都是意外的,因为我不再知道会发生什么)
测试用例4:
foreach($key数组=>;$item){
回显“$item\n”;
每个($阵列);
}
/*产出:12345*/
测试用例5:
foreach($key数组=>;$item){
回显“$item\n”;
重置($阵列);
}
/*产出:12345*/
…没有什么出人意料的,事实上,它似乎支持“复制源”理论
问题
这是怎么回事?我的C-fu不够好,我无法通过查看PHP源代码得出正确的结论,如果有人能帮我翻译成英语,我将不胜感激
在我看来,foreach与数组的副本一起工作,但将源数组的数组指针设置为循环后数组的末尾
- 这是正确的吗?整个故事是这样的吗
- 如果没有,它到底在做什么
- 在
foreach期间使用调整数组指针的函数(each(),reset()等)是否会影响循环的结果
foreach支持对三种不同类型的值进行迭代:
- 阵列
- 正常对象
可遍历的对象
在下面,我将尝试精确地解释迭代在不同情况下是如何工作的。到目前为止,最简单的情况是可遍历的对象,因为对于这些foreach对象,基本上只是这些代码的语法糖:
foreach($k=>;$v){/*…*/}
/*翻译为:*/
if($IteratorAggregate的实例){
$it=$it->;getIterator();
}
对于($it->;倒带();$it->;有效();$it->;下一步()){
$v=$it->;当前();
$k=$it->;键();
/* ... */
}
对于内部类,通过使用内部API来避免实际的方法调用,该API本质上只是镜像C级上的迭代器接口
数组和普通对象的迭代要复杂得多。首先,应该注意的是,在PHP中,“数组”实际上是有序字典,它们将按照这个顺序进行遍历(只要不使用类似于排序的东西,它就与插入顺序相匹配)。这与按键的自然顺序迭代(其他语言中的列表通常如何工作)或完全没有定义的顺序(其他语言中的词典通常如何工作)相反
这同样适用于对象,因为对象属性可以看作是另一个(有序的)字典,将属性名称映射到它们的值,再加上一些可见性处理。在大多数情况下,对象属性实际上并不是以这种相当低效的方式存储的。但是,如果开始迭代对象,通常使用的压缩表示将转换为实际字典。在这一点上,普通对象的迭代变得非常类似于数组的迭代(这就是为什么我在这里不太讨论普通对象迭代的原因)
到目前为止,一切顺利。翻阅字典不会太难吧?当您意识到数组/对象可以在迭代过程中更改时,问题就开始了。有多种方法可以实现这一点:
- 如果您使用
foreach($arr as&;v)通过引用进行迭代,则$arr将转换为引用,您可以在迭代过程中更改它 - 在PHP5中,即使您按值进行迭代,也同样适用,但数组之前是一个引用:
$ref=&$arr;foreach($ref as$v) - 对象具有副句柄传递语义,这在大多数实际情况下意味着它们的行为类似于引用。因此,对象在迭代过程中总是可以更改的
在迭代过程中允许修改的问题是您当前所在的元素被删除的情况。假设使用指针跟踪当前所在的数组元素。如果该元素现在被释放,则留下一个悬空指针(通常会导致segfault)
解决这个问题有不同的方法。PHP5和PHP7在这方面有很大的不同,我将在下面描述这两种行为。总之,PHP5的方法相当愚蠢,会导致各种奇怪的边缘案例问题,而PHP7更复杂的方法会导致更可预测和一致的行为
最后,应该注意,PHP使用引用计数和写时复制来管理内存。这意味着,如果“复制”一个值,实际上只需重用旧值并增加其引用计数(refcount)。只有在您执行某种修改后,才会进行真正的复制(称为“复制”)。看你被骗了,关于这个话题有更广泛的介绍
PHP 5
内部数组指针和哈希指针
PHP5中的数组有一个专用的“内部数组指针”(IAP),它正确地支持修改:每当删除一个元素时,都会检查IAP是否指向该元素。如果是这样,则会前进到下一个元素
虽然foreach确实使用了IAP,但还有一个额外的复杂性:只有一个IAP,但一个数组可以是多个foreach循环的一部分:
//在此处使用by-ref迭代以确保
//两个循环中的数组相同,而不是副本
foreach($arr as&;v1){
外汇($arr as&;v){
// ...
}
}
为了支持只有一个内部数组指针的两个同时循环,foreach执行以下诡计:在执行循环体之前,foreach会将指向当前元素的指针及其散列备份到每个foreachHashPointer中。循环体运行后,如果IAP仍然存在,它将被设置回该元素。但是,如果元素已被删除,我们将只使用IAP当前所在的位置。这个方案基本上是可行的,但是你可以从中得到很多奇怪的行为,我将在下面演示其中的一些
阵列复制
IAP是数组的一个可见功能(通过当前的函数系列公开),因为对IAP的此类更改将被视为写时复制语义下的修改。不幸的是,这意味着在许多情况下,foreach被迫复制它正在迭代的数组。具体条件如下:
- 数组不是引用(is_ref=0)。如果它是一个引用,那么对它的更改应该传播,所以它不应该被复制
- 该数组的refcount>1。如果
refcount为1,则该数组不共享,我们可以直接修改它
如果数组没有重复(is_ref=0,refcount=1),则只有其refcount将递增(*)。阿迪