Skip to content

筛选与映射

概述

array_filterarray_maparray_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 指定初始值。不指定时使用数组的第一个元素,可能导致类型不一致。

最佳实践

  1. 筛选用 array_filter:比 foreach + if 更简洁
  2. 转换用 array_map:比 foreach + array_push 更声明式
  3. 累积用 array_reduce:替代循环中的累加器
  4. 管道式组合:filter -> map -> reduce 链式处理
  5. 保持键名后重建array_values() 重建连续索引
  6. array_filter 传标志:需要键时用 ARRAY_FILTER_USE_BOTH

参考链接