Skip to content

foreach 循环

foreach 是 PHP 中遍历数组和对象最便捷的循环结构。它自动迭代数组中的每个元素,无需手动管理索引或计数器。foreach 适用于索引数组和关联数组,是 PHP 中最常用的循环方式。

前置知识

基础概念

foreach 只能用于遍历数组和对象。当 foreach 开始执行时,数组内部的指针会自动重置到第一个元素。

foreach 有两种语法形式:

  • 仅取值foreach ($array as $value)
  • 取键值对foreach ($array as $key => $value)

不可用于标量值

foreach 不能用于遍历标量值(如整数、浮点数、字符串)。尝试遍历非可迭代值会产生 Warning(PHP 8.0+)或 Error

语法结构

遍历值

php
<?php
declare(strict_types=1);

$fruits = ['苹果', '香蕉', '橙子', '葡萄'];

foreach ($fruits as $fruit) {
    echo $fruit . "\n";
}
// 输出:
// 苹果
// 香蕉
// 橙子
// 葡萄

遍历键值对

php
<?php
declare(strict_types=1);

$user = [
    'name'  => '张三',
    'age'   => 28,
    'email' => 'zhang@example.com',
    'city'  => '北京',
];

foreach ($user as $key => $value) {
    echo "{$key}: {$value}\n";
}
// 输出:
// name: 张三
// age: 28
// email: zhang@example.com
// city: 北京

遍历整数索引数组

php
<?php
declare(strict_types=1);

$scores = [95, 87, 92, 78, 88];

foreach ($scores as $index => $score) {
    echo "学生 #{$index} 成绩:{$score}\n";
}
// 输出:
// 学生 #0 成绩:95
// 学生 #1 成绩:87
// 学生 #2 成绩:92
// 学生 #3 成绩:78
// 学生 #4 成绩:88

foreach 的替代语法

php
<?php
declare(strict_types=1);

$items = [
    ['name' => '笔记本电脑', 'price' => 5999],
    ['name' => '无线鼠标',   'price' => 89],
];
?>
<ul>
    <?php foreach ($items as $item): ?>
        <li>
            <?php echo htmlspecialchars($item['name']); ?>
            - &yen;<?php echo number_format($item['price'], 2); ?>
        </li>
    <?php endforeach; ?>
</ul>

详细说明

通过引用修改元素

使用 &$value 可以在循环体内直接修改原数组的元素。引用遍历时,$value 是数组元素的一个引用,对它的修改会直接影响原数组。

php
<?php
declare(strict_types=1);

$prices = [100, 200, 300, 400];

// 通过引用将每个价格增加 10%
foreach ($prices as &$price) {
    $price = (int) ($price * 1.1);
}
unset($price); // 重要:取消引用,避免后续代码中的意外修改

print_r($prices);
// 输出:Array ( [0] => 110 [1] => 220 [2] => 330 [3] => 440 )

必须使用 unset() 解除引用

在引用遍历结束后,$value 仍然是对数组最后一个元素的引用。如果不使用 unset($value) 解除引用,后续对 $value 的赋值可能会意外修改数组的最后一个元素。

php
<?php
declare(strict_types=1);

// 错误示例:忘记 unset 导致的 bug
$arr = [1, 2, 3];

foreach ($arr as &$value) {
    $value *= 2;
}
// 忘记 unset($value)

$value = 99; // 这会意外修改 $arr[2] 的值!
print_r($arr);
// 输出:Array ( [0] => 2 [1] => 4 [2] => 99 )

// 正确做法
$arr = [1, 2, 3];
foreach ($arr as &$value) {
    $value *= 2;
}
unset($value);

$value = 99; // 现在是安全的,不影响数组
print_r($arr);
// 输出:Array ( [0] => 2 [1] => 4 [2] => 6 )

遍历嵌套数组

嵌套数组(多维数组)需要使用嵌套的 foreach 来遍历。

php
<?php
declare(strict_types=1);

$students = [
    ['name' => '张三', 'scores' => [85, 92, 78]],
    ['name' => '李四', 'scores' => [90, 88, 95]],
    ['name' => '王五', 'scores' => [76, 82, 91]],
];

foreach ($students as $student) {
    echo "学生:{$student['name']}\n";

    foreach ($student['scores'] as $subject => $score) {
        echo "  科目 {$subject}:{$score} 分\n";
    }

    $average = array_sum($student['scores']) / count($student['scores']);
    echo "  平均分:{$average}\n";
}

遍历对象

foreach 可以遍历实现了 Traversable 接口的对象,包括 IteratorIteratorAggregate 以及 ArrayObject 等。

php
<?php
declare(strict_types=1);

class ColorCollection implements \IteratorAggregate
{
    private array $colors = [];

    public function __construct(array $colors)
    {
        $this->colors = $colors;
    }

    public function add(string $color): void
    {
        $this->colors[] = $color;
    }

    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->colors);
    }
}

$collection = new ColorCollection(['红色', '绿色', '蓝色']);
$collection->add('黄色');

foreach ($collection as $color) {
    echo $color . "\n";
}
// 输出:
// 红色
// 绿色
// 蓝色
// 黄色

在循环中修改数组

循环中修改数组的行为

foreach 循环中修改正在遍历的数组可能导致意外的行为。PHP 的 foreach 在遍历开始时会创建数组的内部副本(非引用遍历时),因此在循环体内添加或删除元素不会影响遍历过程。

php
<?php
declare(strict_types=1);

// 在非引用遍历中添加元素不会影响当前遍历
$items = ['a', 'b', 'c'];

foreach ($items as $item) {
    echo $item . "\n";
    if ($item === 'b') {
        $items[] = 'd'; // 添加元素,但不会在当前遍历中被访问
    }
}
// 输出:a, b, c(不包含 d)

// 引用遍历中修改是安全的
$items = ['a', 'b', 'c'];

foreach ($items as $key => &$item) {
    echo $item . "\n";
    if ($item === 'b') {
        $items[$key] = 'B'; // 修改当前元素
    }
}
unset($item);

print_r($items);
// 输出:Array ( [0] => a [1] => B [2] => c )

实战示例

数据过滤和转换

php
<?php
declare(strict_types=1);

function filterAndTransform(array $products, float $minPrice): array
{
    $result = [];

    foreach ($products as $product) {
        if ($product['price'] < $minPrice) {
            continue; // 跳过低价商品
        }

        $result[] = [
            'name'  => strtoupper($product['name']),
            'price' => $product['price'],
            'tax'   => round($product['price'] * 0.13, 2),
        ];
    }

    return $result;
}

$products = [
    ['name' => '手机', 'price' => 3999],
    ['name' => '耳机', 'price' => 199],
    ['name' => '数据线', 'price' => 15],
    ['name' => '平板', 'price' => 2599],
];

$filtered = filterAndTransform($products, 100);
foreach ($filtered as $item) {
    echo "{$item['name']}:价格 {$item['price']},税 {$item['tax']}\n";
}

构建映射表

php
<?php
declare(strict_types=1);

function buildIdNameMap(array $users): array
{
    $map = [];

    foreach ($users as $user) {
        $map[$user['id']] = $user['name'];
    }

    return $map;
}

$users = [
    ['id' => 101, 'name' => '张三'],
    ['id' => 102, 'name' => '李四'],
    ['id' => 103, 'name' => '王五'],
];

$map = buildIdNameMap($users);
echo "ID 101 对应:" . $map[101] . "\n";
echo "ID 103 对应:" . $map[103] . "\n";

分组统计

php
<?php
declare(strict_types=1);

function groupByCategory(array $products): array
{
    $groups = [];

    foreach ($products as $product) {
        $category = $product['category'];

        if (!isset($groups[$category])) {
            $groups[$category] = [
                'count'  => 0,
                'revenue' => 0,
            ];
        }

        $groups[$category]['count']++;
        $groups[$category]['revenue'] += $product['price'] * $product['sold'];
    }

    return $groups;
}

$products = [
    ['category' => '电子', 'price' => 3999, 'sold' => 50],
    ['category' => '电子', 'price' => 899,  'sold' => 120],
    ['category' => '图书', 'price' => 59,   'sold' => 500],
    ['category' => '图书', 'price' => 79,   'sold' => 300],
    ['category' => '服装', 'price' => 199,  'sold' => 200],
];

$groups = groupByCategory($products);

foreach ($groups as $category => $data) {
    echo "分类:{$category}\n";
    echo "  商品数:{$data['count']}\n";
    echo "  销售额:{$data['revenue']}\n";
}

注意事项

  1. 不要使用 list() 解构foreach ($array as list($a, $b)) 可以解构嵌套数组,但在 PHP 8.1 中可以使用更灵活的数组解构语法 [$a, $b]

  2. foreach 不改变数组指针foreach 会自动操作内部数组指针,循环结束后指针会指向最后一个元素。如果需要在循环后重置,使用 reset()

  3. 引用遍历的性能:引用遍历比值遍历略慢,因为 PHP 需要维护引用关系。只在需要修改元素时才使用引用。

  4. 不可遍历标量值:PHP 8.0+ 对尝试遍历 nullfalseintfloatstring 等非可迭代值会产生 TypeError

最佳实践

  1. 优先使用 foreach 遍历数组:比 for 循环更安全、更简洁,不会因为索引管理错误而出 bug。

  2. 引用遍历后必须 unset():防止后续代码中意外修改数组元素。

  3. 避免在循环中增删元素:如果需要过滤数组,先收集要操作的元素,循环结束后再统一处理,或使用 array_filter()array_map() 等函数。

  4. 考虑使用函数式方法:对于简单的过滤和转换,array_filter()array_map()array_reduce() 等函数可能更清晰。

  5. 使用 readonly 防止意外修改(PHP 8.1+):在需要只读遍历时,考虑使用只读属性或不变量来防止意外修改。

下一节

循环语句的学习到此结束。接下来了解跳转与终止语句,首先是 break 和 continue

参考链接