PHP 'foreach' 循环的工作原理详解
PHP ‘foreach’ 循环的工作原理详解
技术背景
在 PHP 中,foreach
是一个非常常用的循环结构,它支持对数组、普通对象和 Traversable
对象进行迭代。然而,其在不同情况下的工作机制较为复杂,特别是在处理数组和对象在迭代过程中可能发生的修改时,PHP 5 和 PHP 7 有显著差异。了解 foreach
的工作原理有助于我们避免一些潜在的问题,编写出更健壮的代码。
实现步骤
1. 支持的迭代类型
foreach
支持对以下三种类型的值进行迭代:
- 数组
- 普通对象
Traversable
对象
2. Traversable
对象的迭代
对于 Traversable
对象,foreach
本质上是以下代码的语法糖:
1 |
|
对于内部类,通过使用内部 API 避免实际的方法调用,该 API 在 C 级别上基本反映了 Iterator
接口。
3. 数组和普通对象的迭代
PHP 中的“数组”实际上是有序字典,会按照插入顺序进行遍历。对象的属性也可以看作是另一个(有序)字典,将属性名映射到其值,并进行一些可见性处理。
PHP 5
- 内部数组指针和 HashPointer:PHP 5 中的数组有一个专用的“内部数组指针”(IAP),支持修改操作。当元素被移除时,会检查 IAP 是否指向该元素,如果是,则将其推进到下一个元素。
foreach
使用 IAP,但由于一个数组可能参与多个foreach
循环,因此foreach
在循环体执行前会将当前元素的指针和哈希备份到每个foreach
的HashPointer
中,循环体执行后,如果元素仍然存在,则将 IAP 设置回该元素,否则使用当前 IAP 所在位置。 - 数组复制:IAP 是数组的可见特征,对 IAP 的更改在写时复制语义下算作修改。因此,在许多情况下,
foreach
被迫复制正在迭代的数组。具体条件为:数组不是引用(is_ref = 0)且引用计数(refcount)大于 1。如果数组未复制(is_ref = 0,refcount = 1),则仅增加其 refcount。此外,如果使用foreach
按引用迭代,则(可能复制的)数组将变为引用。 - 位置推进顺序:
foreach
在循环体运行前就将数组指针向前移动,这意味着当循环体处理元素$i
时,IAP 已经指向元素$i + 1
。
PHP 7
- 哈希表迭代器:PHP 7 支持创建任意数量的外部、安全的哈希表迭代器。这些迭代器必须在数组中注册,从那时起,它们具有与 IAP 相同的语义:如果数组元素被移除,所有指向该元素的哈希表迭代器将被推进到下一个元素。
foreach
不再使用 IAP,因此对current()
等函数的结果没有影响,其自身行为也不会受到reset()
等函数的影响。 - 数组复制:现在,按值迭代数组时,在所有情况下仅增加 refcount(而不是复制数组)。如果在
foreach
循环期间修改了数组,则此时会发生复制(根据写时复制),foreach
将继续处理旧数组。
核心代码
PHP 5 数组复制示例
1 |
|
PHP 7 迭代期间修改数组示例
1 |
|
最佳实践
- 尽量避免在
foreach
循环中修改正在迭代的数组或对象,以免出现意外结果。 - 如果需要修改数组元素,建议使用按引用迭代,但要注意可能带来的问题,如无限循环。
- 在编写代码时,要考虑不同 PHP 版本中
foreach
的行为差异,确保代码在目标环境中正常运行。
常见问题
PHP 5 中的问题
- 嵌套循环问题:在嵌套循环中,由于
HashPointer
的备份和恢复机制,可能会导致外部循环提前终止。 - 数组复制问题:
foreach
可能会不必要地复制数组,违反写时复制语义,导致用户可见的行为变化。
PHP 7 中的问题
- 引用数组按值迭代的行为变化:在 PHP 7 中,引用数组按值迭代时,不再反映迭代期间对数组的修改,这与 PHP 5 不同。
PHP 'foreach' 循环的工作原理详解
https://119291.xyz/posts/php-foreach-loop-working-principle/