PHP 'foreach' 循环的工作原理详解

PHP ‘foreach’ 循环的工作原理详解

技术背景

在 PHP 中,foreach 是一个非常常用的循环结构,它支持对数组、普通对象和 Traversable 对象进行迭代。然而,其在不同情况下的工作机制较为复杂,特别是在处理数组和对象在迭代过程中可能发生的修改时,PHP 5 和 PHP 7 有显著差异。了解 foreach 的工作原理有助于我们避免一些潜在的问题,编写出更健壮的代码。

实现步骤

1. 支持的迭代类型

foreach 支持对以下三种类型的值进行迭代:

  • 数组
  • 普通对象
  • Traversable 对象

2. Traversable 对象的迭代

对于 Traversable 对象,foreach 本质上是以下代码的语法糖:

1
2
3
4
5
6
7
8
9
10
11
12
foreach ($it as $k => $v) { /* ... */ }

/* 转换为: */

if ($it instanceof IteratorAggregate) {
$it = $it->getIterator();
}
for ($it->rewind(); $it->valid(); $it->next()) {
$v = $it->current();
$k = $it->key();
/* ... */
}

对于内部类,通过使用内部 API 避免实际的方法调用,该 API 在 C 级别上基本反映了 Iterator 接口。

3. 数组和普通对象的迭代

PHP 中的“数组”实际上是有序字典,会按照插入顺序进行遍历。对象的属性也可以看作是另一个(有序)字典,将属性名映射到其值,并进行一些可见性处理。

PHP 5

  • 内部数组指针和 HashPointer:PHP 5 中的数组有一个专用的“内部数组指针”(IAP),支持修改操作。当元素被移除时,会检查 IAP 是否指向该元素,如果是,则将其推进到下一个元素。foreach 使用 IAP,但由于一个数组可能参与多个 foreach 循环,因此 foreach 在循环体执行前会将当前元素的指针和哈希备份到每个 foreachHashPointer 中,循环体执行后,如果元素仍然存在,则将 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
2
3
4
5
6
function iterate($arr) {
foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

PHP 7 迭代期间修改数组示例

1
2
3
4
5
6
$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
var_dump($val);
$array[2] = 0;
}

最佳实践

  • 尽量避免在 foreach 循环中修改正在迭代的数组或对象,以免出现意外结果。
  • 如果需要修改数组元素,建议使用按引用迭代,但要注意可能带来的问题,如无限循环。
  • 在编写代码时,要考虑不同 PHP 版本中 foreach 的行为差异,确保代码在目标环境中正常运行。

常见问题

PHP 5 中的问题

  • 嵌套循环问题:在嵌套循环中,由于 HashPointer 的备份和恢复机制,可能会导致外部循环提前终止。
  • 数组复制问题foreach 可能会不必要地复制数组,违反写时复制语义,导致用户可见的行为变化。

PHP 7 中的问题

  • 引用数组按值迭代的行为变化:在 PHP 7 中,引用数组按值迭代时,不再反映迭代期间对数组的修改,这与 PHP 5 不同。

PHP 'foreach' 循环的工作原理详解
https://119291.xyz/posts/php-foreach-loop-working-principle/
作者
ww
发布于
2025年5月16日
许可协议