八重樱:PHP 内核:foreach 是如何工作的(一)​zhuanlan.zhihu.com

PHP 5

内部数组指针和散列指针

PHP 5 中的数组有一个专用的 “内部数组指针”(IAP),它适当地支持修改:每当删除一个元素时,都会检查 IAP 是否指向该元素。 如果是,则转发到下一个元素。

虽然 foreach 确实使用了 IAP,但还有一个复杂因素:只有一个 IAP,但是一个数组可以是多个 foreach 循环的一部分:

// 在这里使用by-ref迭代来确保它真的
// 两个循环中的相同数组而不是副本
foreach ($arr as &$v1) {foreach ($arr as &$v) {// ...}
}

为了支持只有一个内部数组指针的两个同时循环,foreach 执行以下 shenanigans:在执行循环体之前,foreach 将备份指向当前元素及其散列的指针到每个 foreachHashPointer。循环体运行后,如果 IAP 仍然存在,IAP 将被设置回该元素。 但是,如果元素已被删除,我们将只在 IAP 当前所在的位置使用。这个计划基本上是可行的,但是你可以从中获得很多奇怪的情况,其中一些我将在下面演示。

数组复制

IAP 是数组的一个可见特性 (通过 current 系列函数公开),因此 IAP 计数的更改是在写时复制语义下的修改。不幸的是,这意味着 foreach 在许多情况下被迫复制它正在迭代的数组。 具体条件是:

  1. 数组不是引用(is_ref = 0)。 如果它是一个引用,那么对它的更改将被传播,因此不应该复制它。
  2. 数组的 refcount>1。如果 refcount 是 1,那么此数组是不共享的,我们可以直接修改它。

如果数组没有被复制 (is_ref=0, refcount=1),那么只有它的 refcount 会被增加 (*)。此外,如果使用带引用的 foreach,那么 (可能重复的) 数组将转换为引用。

如下代码作为引起复制的示例:

function iterate($arr) {foreach ($arr as $v) {}
}$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

在这里,$arr 将被复制以防止 $arr 上的 IAP 更改泄漏到 $outerArr。 就上述条件而言,数组不是引用(is_ref = 0),并且在两个地方使用(refcount = 2)。 这个要求是不幸的,也是次优实现的工件(这里不需要修改迭代,因此我们不需要首先使用 IAP)。

(*)增加 refcount 听起来无害,但违反了写时复制(COW)语义:这意味着我们要修改 refcount = 2 数组的 IAP,而 COW 则要求只能执行修改 on refcount = 1 值。这种违反会导致用户可见的行为更改 (而 COW 通常是透明的),因为迭代数组上的 IAP 更改将是可见的 -- 但只有在数组上的第一个非 IAP 修改之前。相反,这三个 “有效” 选项是:a) 始终复制,b) 不增加 refcount,从而允许在循环中任意修改迭代数组,c) 完全不使用 IAP (PHP 7 解决方案)。

位置发展顺序

要正确理解下面的代码示例,你必须了解最后一个实现细节。在伪代码中,循环遍历某些数据结构的 “正常” 方法是这样的:

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

然而,foreach,作为一个相当特殊的 snowflake,选择做的事情略有不同:

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

也就是说,数组指针 在循环体运行之前已经向前移动了。这意味着,当循环体处理元素 $i 时,IAP 已经位于元素 $i+1。这就是为什么在迭代期间显示修改的代码示例总是 unset下一个元素,而不是当前元素的原因。

例子:你的测试用例

上面描述的三个方面应该可以让你大致了解 foreach 实现的特性,我们可以继续讨论一些例子。

此时,测试用例的行为更容易理解:

  • 在测试用例 1 和 2 中,$array 以 refcount = 1 开始,因此它不会被 foreach 复制:只有 refcount 才会递增。 当循环体随后修改数组(在该点处具有 refcount = 2)时,将在该点处进行复制。 Foreach 将继续处理未修改的 $array 副本。
  • 在测试用例 3 中,数组没有再被复制,因此 foreach 将修改 $array 变量的 IAP。 在迭代结束时,IAP 为 NULL(意味着迭代已完成),其中 each 返回 false。
  • 在测试用例 4 和 5 中,each 和 reset 都是引用函数。$array 在传递给它们时有一个 refcount = 2,所以必须复制它。因此,foreach 将再次处理一个单独的数组。

例子:current 在 foreach 中的作用

显示各种复制行为的一个好方法是观察 foreach 循环中 current() 函数的行为。看如下这个例子:

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

在这里,你应该知道 current() 是一个 by-ref 函数 (实际上是:preferences-ref),即使它没有修改数组。它必须很好地处理所有其他函数,如 next,它们都是 by-ref。通过引用传递意味着数组必须是分开的,因此 $array 和 foreach-array 将是不同的。你得到是 2 而不是 1 的原因也在上面提到过:foreach在运行用户代码之前指向数组指针,而不是之后。因此,即使代码位于第一个元素,foreach 已经将指针指向第二个元素。

现在让我们尝试一下小修改:

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

这里我们有 is_ref=1 的情况,所以数组没有被复制 (就像上面那样)。但是现在它是一个引用,当传递给 by-ref current() 函数时不再需要复制数组。因此,current() 和 foreach 工作在同一个数组上。不过,由于 foreach 指向指针的方式,你仍可以看到 off-by-one 行为。

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

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

这里重要的部分是,当通过引用迭代 $array 时,foreach 会将 $array 设置为 is_ref=1,所以基本上情况与上面相同。

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

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

这里 $array 的 refcount 在循环开始时是 2,所以这一次我们必须在前面进行复制。因此,$array 和 foreach 使用的数组从一开始就完全分离。这就是为什么 IAP 的位置在循环之前的任何位置 (在本例中是在第一个位置)。

例子:迭代期间的修改

尝试理解迭代过程中的修改是我们所有 foreach 问题的起源,因此我们可以拿一些例子来考虑。

考虑相同数组上的这些嵌套循环 (其中 by-ref 迭代用于确保它确实是相同的):

foreach ($array as &$v1) {foreach ($array as &$v2) {if ($v1 == 1 && $v2 == 1) {unset($array[1]);}echo "($v1, $v2)n";}
}// 输出: (1, 1) (1, 3) (1, 4) (1, 5)

这里的预期部分是输出中缺少 (1,2),因为元素 1 被删除了。可能出乎意料的是,外部循环在第一个元素之后停止。这是为什么呢?

这背后的原因是上面描述的嵌套循环攻击:在循环体运行之前,当前 IAP 位置和散列被备份到一个 HashPointer 中。在循环体之后,它将被恢复,但是只有当元素仍然存在时,否则将使用当前 IAP 位置 (无论它是什么)。在上面的例子中,情况正是这样:外部循环的当前元素已经被删除,所以它将使用 IAP,而内部循环已经将 IAP 标记为 finished !

HashPointer 备份 + 恢复机制的另一个结果是,通过 reset() 等方法更改 IAP。通常不会影响 foreach。例如,下面的代码执行起来就像根本不存在 reset() 一样:

$array = [1, 2, 3, 4, 5];

foreach ($array as &$value) {

var_dump($value);

reset($array);

}

// 输出: 1, 2, 3, 4, 5

原因是,当 reset() 暂时修改 IAP 时,它将恢复到循环体后面的当前 foreach 元素。要强制 reset() 对循环产生影响,你必须删除当前元素,这样备份 / 恢复机制就会失败:

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

但是,这些例子仍是合理的。如果你还记得 HashPointer 还原使用指向元素及其散列的指针来确定它是否仍然存在,那么真正的乐趣就开始了。但是:散列有冲突,指针可以重用!这意味着,通过仔细选择数组键,我们可以让 foreach 相信被删除的元素仍然存在,因此它将直接跳转到它。一个例子:

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

这里根据前面的规则,我们通常期望输出 1,1,3,4。实际情况上'FYFY' 具有与删除的元素'FYFY' 相同的散列,而分配器恰好重用相同的内存位置来存储元素。因此,foreach 最终直接跳转到新插入的元素,从而缩短了循环。

在循环期间替换迭代实体

我想提到的最后一个奇怪的情况是,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;}
}
/* 输出: 1 2 3 6 7 8 9 10 */

正如你在本例中所看到的,一旦替换发生,PHP 将从头开始迭代另一个实体。

PHP 7

散列表迭代器

如果你还记得,数组迭代的主要问题是如何处理迭代过程中元素的删除。PHP 5 为此使用了一个内部数组指针 (IAP),这有点不太理想,因为一个数组指针必须被拉伸以支持多个同时进行的 foreach 循环和与 reset() 等的交互。最重要的是。

PHP 7 使用了一种不同的方法,即支持创建任意数量的外部安全散列表迭代器。这些迭代器必须在数组中注册,从这一点开始,它们具有与 IAP 相同的语义:如果删除了一个数组元素,那么指向该元素的所有 hashtable 迭代器都将被提升到下一个元素。

这意味着 foreach 将不再使用 IAP。foreach 循环绝对不会影响 current() 等的结果。它自己的行为永远不会受到像 reset() 等函数的影响。

数组复制

PHP 5 和 PHP 7 之间的另一个重要更改与数组复制有关。现在 IAP 不再使用了,在所有情况下,按值数组迭代将只执行 refcount 增量 (而不是复制数组)。如果数组在 foreach 循环期间被修改,那么此时将发生复制 (根据写时复制),而 foreach 将继续处理旧数组。

在大多数情况下,这种更改是透明的,除了更好的性能之外没有其他效果。但是,有一种情况会导致不同的行为,即数组前是一个引用:

$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {var_dump($val);$array[2] = 0;
}
/* 旧输出: 1, 2, 0, 4, 5 */
/* 新输出: 1, 2, 3, 4, 5 */

以前,引用数组的按值迭代是一种特殊情况。在本例中,没有发生重复,因此在迭代期间对数组的所有修改都将由循环反映出来。在 PHP 7 中,这种特殊情况消失了:数组的按值迭代将始终继续处理原始元素,而不考虑循环期间的任何修改。

当然,这不适用于 by-reference 迭代。如果你通过引用进行迭代,那么所有的修改都将被循环所反映。有趣的是,对于普通对象的按值迭代也是如此:

$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {var_dump($val);$obj->bar = 42;
}
/* 新旧输出: 1, 42 */

这反映了对象的按句柄语义 (即,即使在按值上下文中,它们的行为也类似于引用)。

例子

让我们考虑几个例子,从你的测试用例开始:

测试用例 1 和 2 输出相同:按值数组迭代始终在原始元素上工作。(在本例中,甚至 refcounting 和复制行为在 PHP 5 和 PHP 7 之间也是完全相同的)。

测试用例 3 的变化:Foreach 不再使用 IAP,因此 each() 不受循环影响。前后输出一样。

测试用例 4 和 5 保持不变:each() 和 reset() 将在更改 IAP 之前复制数组,而 foreach 仍然使用原始数组。(即使数组是共享的,IAP 的更改也无关紧要。)

第二组示例与 current() 在不同 reference/refcounting 配置下的行为有关。这不再有意义,因为 current() 完全不受循环影响,所以它的返回值总是保持不变。

然而,当考虑迭代过程中的修改时,我们得到了一些有趣的变化。我希望你会发现新的行为更加健全。 第一个例子:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {foreach ($array as &$v2) {if ($v1 == 1 && $v2 == 1) {unset($array[1]);}echo "($v1, $v2)n";}
}// 旧输出: (1, 1) (1, 3) (1, 4) (1, 5)
// 新输出: (1, 1) (1, 3) (1, 4) (1, 5)
//        (3, 1) (3, 3) (3, 4) (3, 5)
//        (4, 1) (4, 3) (4, 4) (4, 5)
//        (5, 1) (5, 3) (5, 4) (5, 5) 

如你所见,外部循环在第一次迭代之后不再中止。原因是现在两个循环都有完全独立的 hashtable 散列表迭代器,并且不再通过共享的 IAP 对两个循环进行交叉污染。

现在修复的另外一个奇怪的边缘现象是,当删除并且添加恰好具有相同的哈希元素时,会得到奇怪的结果:

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

之前的 HashPointer 恢复机制直接跳转到新元素,因为它 “看起来” 和删除的元素相同(由于哈希和指针冲突)。由于我们不再依赖于哈希元素,因此不再是一个问题。

foreach判断最后一个_PHP 内核:foreach 是如何工作的(二)相关推荐

  1. foreach判断最后一个_JavaScript很简单?那你理解的forEach真的对吗?

    你理解的Array.prototype.forEach真的对吗? Array.prototype.forEach 我们都知道,forEach() 方法对数组的每个元素执行一次给定的函数.它的语法也很简 ...

  2. foreach判断最后一个_ArrayList集合为什么不能使用foreach增删改?

    点击上方"Java技术前线",选择"置顶或者星标" 与你一起成长 译者:奋斗的小程序员链接:http://suo.im/4XaI8Q 编程过程中常常需要使用到集 ...

  3. jquery数组去重:纯数字数组去重,forEach判断数组是否含有该字段,单个数组通过某个字段名去重value值,多个数组合并同时根据某个字段进行去重

    纯数字数组去重 const numbers = [1, 1, 20, 3, 3, 3, 9, 9]; const uniqueNumbers = [...new Set(numbers)] //输出u ...

  4. php 判断5张牌是不是顺子,从扑克牌中随机抽取5张牌,判断是不是一个顺子,即这5张牌是不是连续(面试题)...

    从扑克牌中随机抽取5张牌,判断是不是一个顺子,即这5张牌是不是连续的2-10位数字本身,A为1,J为11,Q为12,K为13,而大小王可以看成任意数字 统计数组中出现的次数,判断等于0,大于1大于2大 ...

  5. java foreach order_Java 8流中的forEach vs forEachOrdered

    我知道这些方法的执行顺序不同,但在所有测试中,我都无法实现不同的执行顺序. 例子: System.out.println("forEach Demo"); Stream.of(&q ...

  6. foreach php,详解PHP中foreach的用法和实例

    本篇文章介绍了详解PHP中foreach的用法和实例,详细介绍了foreach的用法,感兴趣的小伙伴们可以参考一下. 在PHP中经常会用到foreach的使用,而要用到foreach,就必须用到数组. ...

  7. php foreach 修改数组,php如何使用foreach修改数组

    php foreach修改数组的方法:首先创建一个PHP示例文件:然后通过foreach语句"foreach($array as $k => $v){$v = 1;}"修改指 ...

  8. php的foreach什么意思,php中foreach的用法是什么,php foreach as

    php中foreach的用法是什么PHP中foreach的用法是什么,foreach在php中的用法是:[foreach($ array as $ value){要执行的代码:}],对于每个循环,当前 ...

  9. php foreach遍历数组详解,PHP foreach遍历数组详解

    什么是PHP foreach? foreach 是一种遍历数组简便方法.foreach 仅能用于数组,当试图将其用于其它数据类型或者一个未初始化的变量时会产生错误. PHP foreach两种语法 有 ...

最新文章

  1. 计算机科学与技术类高水平国际学术刊物,莘莘学子 | 计算机科学与技术学院本科生薛传雨在国际期刊上发表高水平学术论文...
  2. Replication的犄角旮旯(五)--关于复制identity列
  3. 最新发布丨游戏市场“超预期”增长背景下,如何加速产品精品化 运营精细化?
  4. JUC.Condition学习笔记[附详细源码解析]
  5. CodeForces - 1252E Songwriter(贪心)
  6. java 并发锁_Java并发教程–锁定:内在锁
  7. 实验一 线性表的顺序存储与实现_【自考】数据结构中的线性表,期末不挂科指南,第2篇
  8. 利用filter替换字符串中的空格
  9. 根据mysql生成数据库设计文档_通过navicat工具导出数据库的word格式的设计文档...
  10. python统计表中单词及其出现的次数 字典形式输出_Python统计文本词汇出现次数的实例代码...
  11. DevOps 实践指南
  12. SEAIR传染病模型及其开源代码
  13. spring-boot-starter-data-jpa详细使用介绍
  14. 凯文·凯利:AI将改变一切设计工作
  15. Ethernet0没有有效的IP配置
  16. 【深度学习】深度学习如何影响运筹学?
  17. php 统计 字数,PHP 仿 Word 统计文章字数
  18. 短信包对比 - 2021.5
  19. 相关方管理---章节练习
  20. Android SDK简介

热门文章

  1. (自己测试没有问题)解决git clone时报错:The requested URL returned error: 401 Unauthorized while accessing
  2. PHP操作mongoDB:conn crud
  3. MySQL配置mycat读写分离:wrapper | Startup failed: Timed out waiting for signal from JVM.
  4. PHP操作使用Redis
  5. Yii的各种query
  6. 微信小程序点击button按钮后重置输入框等表单内容
  7. mysql事务 可见性,【每日阅读】2020年12月09日-事务先后的可见性
  8. MySQL优化INSERT的性能
  9. javascript中的setTimeout() 方法和clearInterval() 方法和setInterval() 方法
  10. 如何将python项目部署到服务器_部署python项目到linux服务器