Skip to content

数组遍历

概述

PHP 提供了多种遍历数组的方式,foreach 是最常用和推荐的方式。本章详细讲解 foreach 遍历、list/短语法解构、array_walkarray_maparray_filter 同时获取键值,以及 each() 已移除的说明。

基础概念

方式获取键获取值可修改原数组适用场景
foreach是(引用)通用遍历
array_walk是(引用)原地修改
array_map否(需 array_keys)转换映射
array_filter需 ARRAY_FILTER_USE_BOTH筛选过滤
for通过 count通过索引索引数组
each()PHP 8.0+ 已移除

each() 已移除

each() 函数在 PHP 7.2 被废弃,PHP 8.0 完全移除。请使用 foreach 替代。

语法与代码

foreach 基本遍历

php
<?php

declare(strict_types=1);

// 遍历值
$fruits = ['apple', 'banana', 'cherry'];
foreach ($fruits as $fruit) {
    echo $fruit . "\n";
}

// 遍历键值对
$user = ['name' => 'Alice', 'age' => 30, 'email' => 'a@b.com'];
foreach ($user as $key => $value) {
    echo "{$key}: {$value}\n";
}

// PHP 7.0+ 引用遍历(可修改原数组)
$prices = [100, 200, 300];
foreach ($prices as &$price) {
    $price *= 0.9;  // 打九折
}
unset($price);  // 重要:解除引用

print_r($prices);
// [90, 180, 270]

foreach 解构遍历

php
<?php

declare(strict_types=1);

// PHP 7.1+ 短语法解构
$users = [
    ['name' => 'Alice', 'age' => 30],
    ['name' => 'Bob', 'age' => 25],
    ['name' => 'Charlie', 'age' => 35],
];

foreach ($users as ['name' => $name, 'age' => $age]) {
    echo "{$name} is {$age} years old\n";
}

// PHP 7.1+ list() 解构(也兼容)
foreach ($users as list('name' => $name, 'age' => $age)) {
    echo "{$name}: {$age}\n";
}

// 跳过元素
$records = [[1, 'Alice', 30], [2, 'Bob', 25]];
foreach ($records as [$id, $name]) {
    echo "#{$id}: {$name}\n";
}

array_walk — 原地修改

php
<?php

declare(strict_types=1);

// array_walk — 对每个元素执行回调(可修改原数组)
$data = ['name' => 'alice', 'email' => 'ALICE@EXAMPLE.COM'];

// 值引用修改
array_walk($data, function (mixed &$value, string $key): void {
    $value = ucfirst($value);
});
print_r($data);
// ['name' => 'Alice', 'email' => 'ALICE@example.com']

// array_walk 配合引用修改多维数组
$items = [
    ['name' => 'item1', 'price' => 100],
    ['name' => 'item2', 'price' => 200],
];

array_walk($items, function (array &$item): void {
    $item['price'] *= 1.1;  // 加价 10%
    $item['final'] = true;
});
print_r($items);

array_map — 转换映射

php
<?php

declare(strict_types=1);

// array_map — 对每个元素应用回调,返回新数组
$numbers = [1, 2, 3, 4, 5];
$doubled = array_map(fn(int $n): int => $n * 2, $numbers);
// [2, 4, 6, 8, 10]

// 同时处理键和值
$user = ['name' => 'alice', 'age' => '30'];

// array_map 默认不传键,需要额外处理
$mapped = array_map(fn(string $key, mixed $val): string => ucfirst($key) . ': ' . $val,
    array_keys($user),
    $user
);
// ['Name: alice', 'Age: 30']

array_filter 同时获取键值(PHP 5.6+)

php
<?php

declare(strict_types=1);

// array_filter — 使用 ARRAY_FILTER_USE_BOTH 同时获取键和值
$products = [
    'apple' => ['price' => 5, 'stock' => 100],
    'banana' => ['price' => 3, 'stock' => 0],
    'cherry' => ['price' => 8, 'stock' => 50],
];

// 过滤有库存且价格 > 4 的商品
$available = array_filter(
    $products,
    fn(string $key, array $value): bool => $value['stock'] > 0 && $value['price'] > 4,
    ARRAY_FILTER_USE_BOTH
);

print_r($available);
// ['apple' => [...], 'cherry' => [...]]

for 循环遍历索引数组

php
<?php

declare(strict_types=1);

$items = ['a', 'b', 'c', 'd', 'e'];
$count = count($items);

for ($i = 0; $i < $count; $i++) {
    echo "{$i}: {$items[$i]}\n";
}

// 需要索引时使用 for
// 注意:count() 在循环前调用一次(不在条件中每次调用)

详细说明

foreach 引用陷阱

php
<?php

declare(strict_types=1);

// 常见陷阱:引用遍历后未 unset
$colors = ['red', 'green', 'blue'];

foreach ($colors as &$color) {
    $color = strtoupper($color);
}
// 如果这里没有 unset($color)...
unset($color);  // 必须解除引用

// 继续使用 $color 不会意外修改数组最后一个元素
$color = 'yellow';
print_r($colors);
// ['RED', 'GREEN', 'BLUE'](不受影响)

遍历中删除元素

php
<?php

declare(strict_types=1);

// 安全方式:先收集要删除的键,再统一删除
$data = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4];
$toRemove = [];

foreach ($data as $key => $value) {
    if ($value % 2 === 0) {
        $toRemove[] = $key;
    }
}

foreach ($toRemove as $key) {
    unset($data[$key]);
}
// ['a' => 1, 'c' => 3]

// 也可以用 array_filter
$data = array_filter($data, fn(int $v): bool => $v % 2 !== 0);

实战示例

数据转换管道

php
<?php

declare(strict_types=1);

$employees = [
    ['name' => 'Alice', 'salary' => 5000, 'department' => 'engineering'],
    ['name' => 'Bob', 'salary' => 3000, 'department' => 'marketing'],
    ['name' => 'Charlie', 'salary' => 7000, 'department' => 'engineering'],
    ['name' => 'Diana', 'salary' => 4000, 'department' => 'marketing'],
];

// 筛选工程部员工
$engineers = array_filter(
    $employees,
    fn(array $e): bool => $e['department'] === 'engineering'
);

// 提取姓名和年薪
$result = array_map(
    fn(array $e): array => [
        'name' => $e['name'],
        'annual' => $e['salary'] * 12,
    ],
    array_values($engineers)
);

print_r($result);
// [['name' => 'Alice', 'annual' => 60000], ['name' => 'Charlie', 'annual' => 84000]]

array_column 提取数据

php
<?php

declare(strict_types=1);

$users = [
    ['id' => 1, 'name' => 'Alice', 'role' => 'admin'],
    ['id' => 2, 'name' => 'Bob', 'role' => 'user'],
    ['id' => 3, 'name' => 'Charlie', 'role' => 'user'],
];

// 提取单列
$names = array_column($users, 'name');
// ['Alice', 'Bob', 'Charlie']

// 指定键列
$byId = array_column($users, 'name', 'id');
// [1 => 'Alice', 2 => 'Bob', 3 => 'Charlie']

// 提取对象数组列
class Product
{
    public function __construct(public string $name, public float $price) {}
}

$products = [new Product('Apple', 3.5), new Product('Banana', 2.0)];
$names = array_column($products, 'name');
// ['Apple', 'Banana'](PHP 7.0+ 支持对象数组)

array_map 同时处理键值

php
<?php

declare(strict_types=1);

// array_map 同时处理键和值的技巧

$config = ['debug' => true, 'cache_time' => 3600, 'max_connections' => 100];

// 方式 1:使用 array_map + array_keys
$result = array_map(
    fn(string $key, mixed $val): string => strtoupper($key) . ': ' . var_export($val, true),
    array_keys($config),
    $config
);
// ['DEBUG: true', 'CACHE_TIME: 3600', 'MAX_CONNECTIONS: 100']

// 方式 2:使用 array_walk(原地修改)
array_walk($config, function (mixed &$val, string $key): void {
    $val = strtoupper($key) . ': ' . var_export($val, true);
});

// 方式 3:使用 foreach(最灵活)
$result = [];
foreach ($config as $key => $val) {
    $result[strtoupper($key)] = is_bool($val) ? ($val ? 'yes' : 'no') : $val;
}

生成器与数组遍历

php
<?php

declare(strict_types=1);

// 使用生成器进行惰性遍历
function lazyFilter(iterable $items, callable $callback): \Generator
{
    foreach ($items as $key => $item) {
        if ($callback($item)) {
            yield $key => $item;
        }
    }
}

function lazyMap(iterable $items, callable $callback): \Generator
{
    foreach ($items as $key => $item) {
        yield $key => $callback($item);
    }
}

// 大数据集处理
$data = range(1, 1000000);

$result = lazyFilter(
    lazyMap($data, fn(int $n): int => $n * 2),
    fn(int $n): bool => $n > 100 && $n % 3 === 0
);

// 只在需要时计算,内存占用极小
$count = 0;
foreach ($result as $value) {
    $count++;
    if ($count > 10) break; // 只取前 10 个
}
echo "Found at least 10 items\n";

性能建议

对于超大数据集(百万级),使用生成器代替数组遍历,避免内存溢出。生成器只在需要时计算每个元素。

遍历中的内存优化

php
<?php

declare(strict_types=1);

// yield 生成器减少内存
function readLargeFile(string $path): \Generator
{
    $handle = fopen($path, 'r');
    if (!$handle) return;

    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    fclose($handle);
}

// 逐行处理大文件,内存占用极小
$count = 0;
foreach (readLargeFile('/var/log/syslog') as $line) {
    if (str_contains($line, 'ERROR')) {
        $count++;
    }
}
echo "Error lines: {$count}\n";

foreach vs for

foreach 更简洁安全,for 在需要索引时更直观。优先使用 foreach

注意事项

foreach 引用

使用引用遍历时,循环结束后务必 unset($ref) 解除引用,否则后续对 $ref 的赋值会修改数组最后一个元素。

遍历中修改数组

foreach 遍历中直接 unset() 元素是安全的,但添加元素的行为不确定。建议先收集操作,遍历结束后统一执行。

最佳实践

  1. 优先使用 foreach:最简洁、最安全的遍历方式
  2. 遍历多维用解构foreach ($arr as ['key' => $val])
  3. 原地修改用 array_walk:需要修改原数组时
  4. 转换用 array_map:需要返回新数组时
  5. 筛选用 array_filter:保留/过滤元素
  6. 需要键值用 ARRAY_FILTER_USE_BOTH

参考链接