筛选与映射
概述
array_filter、array_map 和 array_reduce 是 PHP 函数式编程的三大核心工具。它们支持对数组进行筛选、转换和归约操作,可以链式组合实现管道式数据处理。本章详细讲解这三个函数的用法和实战模式。
基础概念
| 函数 | 输入 | 输出 | 用途 |
|---|---|---|---|
array_filter | 数组 + 回调 | 过滤后的数组 | 筛选保留/排除 |
array_map | 数组 + 回调 | 转换后的数组 | 逐元素转换 |
array_reduce | 数组 + 回调 + 初始值 | 单个值 | 归约/累积 |
语法与代码
array_filter — 筛选过滤
php
<?php
declare(strict_types=1);
// 基本过滤 - 回调返回 true 保留
$numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
$evens = array_filter($numbers, fn(int $n): bool => $n % 2 === 0);
// [2, 4, 6, 8, 10]
// 过滤空值(不传回调,过滤掉 false/0/''/null/'0'/[])
$data = [0, 'hello', '', null, false, 'world', []];
$filtered = array_filter($data);
// [1 => 'hello', 5 => 'world']
// ARRAY_FILTER_USE_KEY - 按键过滤
$config = ['debug' => true, 'cache' => true, 'log' => false, 'trace' => false];
$active = array_filter($config, fn(string $key): bool => in_array($key, ['debug', 'cache']),
ARRAY_FILTER_USE_KEY);
// ['debug' => true, 'cache' => true]
// ARRAY_FILTER_USE_BOTH - 同时获取键和值
$products = [
'apple' => ['price' => 5, 'stock' => 0],
'banana' => ['price' => 3, 'stock' => 100],
'cherry' => ['price' => 8, 'stock' => 50],
];
$available = array_filter($products,
fn(string $key, array $val): bool => $val['stock'] > 0 && $val['price'] < 10,
ARRAY_FILTER_USE_BOTH
);
// ['banana' => [...], 'cherry' => [...]]array_map — 转换映射
php
<?php
declare(strict_types=1);
// 基本转换
$numbers = [1, 2, 3, 4, 5];
$squared = array_map(fn(int $n): int => $n ** 2, $numbers);
// [1, 4, 9, 16, 25]
// 多数组映射 - 回调接收多个参数
$names = ['Alice', 'Bob', 'Charlie'];
$ages = [30, 25, 35];
$combined = array_map(
fn(string $name, int $age): array => ['name' => $name, 'age' => $age],
$names,
$ages
);
// [['name' => 'Alice', 'age' => 30], ...]
// 使用 null 回调 - 合并多个数组为多维数组
$cols = array_map(null, $names, $ages);
// [['Alice', 30], ['Bob', 25], ['Charlie', 35]]
// 注意:array_map 不保留键名!
$assoc = ['a' => 1, 'b' => 2, 'c' => 3];
$mapped = array_map(fn(int $v): int => $v * 2, $assoc);
// ['a' => 2, 'b' => 4, 'c' => 6](键名保留,但回调只接收值)
// 如果需要同时处理键和值
$mapped = array_map(
fn(int $val, string $key): string => "{$key}: {$val}",
$assoc,
array_keys($assoc)
);
// ['a: 1', 'b: 2', 'c: 3'](但键变成了数字)array_reduce — 归约累积
php
<?php
declare(strict_types=1);
// array_reduce(array $array, callback $callback, mixed $initial): mixed
// callback: fn(mixed $carry, mixed $item): mixed
// 求和
$numbers = [1, 2, 3, 4, 5];
$sum = array_reduce($numbers, fn(int $carry, int $item): int => $carry + $item, 0);
// 15
// 字符串连接
$words = ['Hello', 'World', 'PHP'];
$sentence = array_reduce($words, fn(string $carry, string $item): string =>
$carry === '' ? $item : $carry . ' ' . $item, '');
// 'Hello World PHP'
// 构建关联数组
$records = [
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob'],
['id' => 3, 'name' => 'Charlie'],
];
$byId = array_reduce($records, function (array $carry, array $item): array {
$carry[$item['id']] = $item['name'];
return $carry;
}, []);
// [1 => 'Alice', 2 => 'Bob', 3 => 'Charlie']
// 求最大值
$max = array_reduce($numbers, fn(int $carry, int $item): int => max($carry, $item), PHP_INT_MIN);
// 5管道式处理
php
<?php
declare(strict_types=1);
// 组合 filter -> map -> reduce 实现管道式处理
$orders = [
['product' => 'A', 'price' => 100, 'quantity' => 2],
['product' => 'B', 'price' => 50, 'quantity' => 5],
['product' => 'C', 'price' => 200, 'quantity' => 1],
['product' => 'D', 'price' => 30, 'quantity' => 10],
];
// 筛选:单价 > 50 的订单
$filtered = array_filter($orders, fn(array $o): bool => $o['price'] > 50);
// 映射:计算每个订单金额
$mapped = array_map(fn(array $o): array => [
'product' => $o['product'],
'total' => $o['price'] * $o['quantity'],
], array_values($filtered));
// 归约:计算总金额
$total = array_reduce($mapped, fn(int $carry, array $o): int => $carry + $o['total'], 0);
// A: 200 + C: 200 = 400
echo "符合条件的订单总金额: {$total}";详细说明
array_filter 保持键名
array_filter 会保持原数组的键名。如果不希望保留原始键,可以使用 array_values() 重建索引。
php
<?php
declare(strict_types=1);
$nums = [10, 20, 30, 40, 50];
$filtered = array_filter($nums, fn(int $n): bool => $n > 25);
// [2 => 30, 3 => 40, 4 => 50](保留原键)
$reindexed = array_values($filtered);
// [30, 40, 50](重建为 0, 1, 2)性能建议
array_filter + array_values 组合比 foreach 手动构建更简洁,但性能略低。大数据量(100万+)时考虑直接使用 foreach。
实战示例
统计分析管道
php
<?php
declare(strict_types=1);
function analyzeData(array $records): array
{
$valid = array_filter($records, fn(array $r): bool =>
isset($r['value']) && is_numeric($r['value'])
);
$values = array_map(fn(array $r): float => (float)$r['value'],
array_values($valid));
return [
'count' => count($values),
'sum' => array_sum($values),
'avg' => count($values) > 0 ? array_sum($values) / count($values) : 0,
'max' => count($values) > 0 ? max($values) : 0,
'min' => count($values) > 0 ? min($values) : 0,
];
}
$data = [
['value' => '10'], ['value' => '20'], ['invalid' => true], ['value' => '30'],
];
print_r(analyzeData($data));
// ['count' => 3, 'sum' => 60, 'avg' => 20, 'max' => 30, 'min' => 10]array_column 的筛选和映射组合
php
<?php
declare(strict_types=1);
$orders = [
['id' => 1, 'product' => 'A', 'price' => 100, 'qty' => 2],
['id' => 2, 'product' => 'B', 'price' => 50, 'qty' => 5],
['id' => 3, 'product' => 'C', 'price' => 200, 'qty' => 1],
['id' => 4, 'product' => 'D', 'price' => 30, 'qty' => 10],
['id' => 5, 'product' => 'E', 'price' => 75, 'qty' => 3],
];
// 计算每笔订单金额
$withTotal = array_map(fn(array $o): array => [
'id' => $o['id'],
'product' => $o['product'],
'total' => $o['price'] * $o['qty'],
], $orders);
// 筛选金额 > 200 的订单
$highValue = array_filter(
array_values($withTotal),
fn(array $o): bool => $o['total'] > 200
);
// 计算总金额
$grandTotal = array_reduce($withTotal, fn(int $carry, array $o): int => $carry + $o['total'], 0);
echo "High value orders:\n";
print_r($highValue);
echo "Grand total: {$grandTotal}\n";array_reduce 构建复杂数据结构
php
<?php
declare(strict_types=1);
// 分组统计
$sales = [
['dept' => 'engineering', 'amount' => 10000],
['dept' => 'marketing', 'amount' => 5000],
['dept' => 'engineering', 'amount' => 8000],
['dept' => 'marketing', 'amount' => 6000],
['dept' => 'engineering', 'amount' => 12000],
];
$summary = array_reduce($sales, function (array $carry, array $sale): array {
$dept = $sale['dept'];
if (!isset($carry[$dept])) {
$carry[$dept] = ['total' => 0, 'count' => 0];
}
$carry[$dept]['total'] += $sale['amount'];
$carry[$dept]['count']++;
return $carry;
}, []);
print_r($summary);
// ['engineering' => ['total' => 30000, 'count' => 3], 'marketing' => ['total' => 11000, 'count' => 2]]
// 构建树形结构
$flat = [
['id' => 1, 'parent' => null, 'name' => 'Root'],
['id' => 2, 'parent' => 1, 'name' => 'Child 1'],
['id' => 3, 'parent' => 1, 'name' => 'Child 2'],
['id' => 4, 'parent' => 2, 'name' => 'Grandchild 1'],
['id' => 5, 'parent' => 3, 'name' => 'Grandchild 2'],
];
$tree = array_reduce($flat, function (array $tree, array $item): array {
$node = ['id' => $item['id'], 'name' => $item['name'], 'children' => []];
if ($item['parent'] === null) {
$tree[$item['id']] = $node;
} else {
// 简化:这里需要递归查找父节点
// 实际项目中可以用引用解决
}
return $tree;
}, []);管道式处理模式
php
<?php
declare(strict_types=1);
// 通用管道
class Pipeline
{
private array $operations = [];
public static function from(array $data): self
{
$self = new self();
$self->operations[] = fn(): array => $data;
return $self;
}
public function filter(callable $callback): self
{
$last = array_pop($this->operations);
$this->operations[] = fn(): array => array_values(array_filter($last(), $callback));
return $this;
}
public function map(callable $callback): self
{
$last = array_pop($this->operations);
$this->operations[] = fn(): array => array_map($callback, $last());
return $this;
}
public function take(int $n): self
{
$last = array_pop($this->operations);
$this->operations[] = fn(): array => array_slice($last(), 0, $n);
return $this;
}
public function collect(): array
{
$result = $this->operations[0]();
return $result;
}
}
$result = Pipeline::from(range(1, 100))
->filter(fn(int $n): bool => $n % 2 === 0)
->map(fn(int $n): int => $n ** 2)
->take(5)
->collect();
print_r($result);
// [4, 16, 36, 64, 100]性能权衡
管道式处理代码更声明式、更易读,但每次 filter/map 都创建新数组,有内存和性能开销。对于超大数据集(100万+),直接使用 foreach 循环更高效。
array_walk 递归处理多维数组
php
<?php
declare(strict_types=1);
// array_walk_recursive - 递归遍历多维数组
$data = [
'user' => ['name' => 'alice', 'email' => 'ALICE@B.COM'],
'meta' => ['tags' => ['php', 'laravel'], 'count' => 2],
];
array_walk_recursive($data, function (mixed &$value, string $key): void {
if (is_string($value) && !in_array($key, ['tags', 'count'])) {
$value = ucfirst($value);
}
});
print_r($data);
// ['user' => ['name' => 'Alice', 'email' => 'ALICE@B.COM'], ...]array_map 的实际应用模式
php
<?php
declare(strict_types=1);
// 批量格式化数据
$records = [
['name' => ' alice ', 'email' => 'ALICE@EXAMPLE.COM', 'age' => ' 30 '],
['name' => ' bob ', 'email' => 'BOB@EXAMPLE.COM', 'age' => ' 25 '],
];
// 多步处理:清洗 -> 格式化 -> 转换
$cleaned = array_map(function (array $record): array {
return [
'name' => trim(ucfirst($record['name'])),
'email' => strtolower(trim($record['email'])),
'age' => (int)trim($record['age']),
];
}, $records);
print_r($cleaned);
// 使用数组解构简化
$names = array_map(
fn(array $r): string => ucfirst(trim($r['name'])),
$records
);
// ['Alice', 'Bob']函数式 vs 命令式
函数式风格(array_map/array_filter/array_reduce)代码更简洁、声明式。命令式风格(foreach)更灵活、性能更好。根据场景选择。
注意事项
array_map 不传键
array_map 的回调只接收值参数。如果需要同时处理键和值,需要额外传递 array_keys() 或使用 array_filter + ARRAY_FILTER_USE_BOTH。
array_reduce 初始值
始终为 array_reduce 指定初始值。不指定时使用数组的第一个元素,可能导致类型不一致。
最佳实践
- 筛选用 array_filter:比 foreach + if 更简洁
- 转换用 array_map:比 foreach + array_push 更声明式
- 累积用 array_reduce:替代循环中的累加器
- 管道式组合:filter -> map -> reduce 链式处理
- 保持键名后重建:
array_values()重建连续索引 - array_filter 传标志:需要键时用
ARRAY_FILTER_USE_BOTH