登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

PHP array_reduce 怎样累积生成分组结果

来源:17golang原创

时间:2026-10-26 22:50:25 398浏览 收藏

我第一次用 array_reduce 整理订单接口时,想要的结果是“按商品类别分组”,写出来却只有一条记录。后来发现,array_reduce 不会自动理解“分组”:它只负责把每一轮回调返回的值交给下一轮。只要把 $carry 设计成分组数组,并在每轮返回它,就能稳定累积结果。

官方地址:https://www.php.net/manual/en/function.array-reduce.php

分组的核心写法是:用 [] 作为 initial,用记录中的分类字段作为累加器键;遇到新键就创建分组,遇到旧键就向该分组的 items 追加当前记录,最后返回完整的 $carry。

先把 carry 看成“正在生成的结果”

array_reduce($array, $callback, $initial) 每次把两个值交给回调:上一次回调返回的 $carry,以及当前遍历到的 $item。第一轮的 $carry 来自 $initial。因此,分组场景中的累加器不应该是一个数字,而应该是一个可以不断添加分组的数组。

PHP array_reduce 从输入记录经过 carry 和当前项累积为分组结果的结构图
图1:array_reduce 的 carry 累积链路说明图,展示重复分类如何回到同一个分组桶;这是静态说明图,不是运行截图。

我现在会先写出结果结构,再写回调。假设接口需要下面这样的结果:

 [
        'category' => 'book',
        'items' => [/* 订单记录 */],
        'total_amount' => 0,
    ],
];
?>

这个结构有两个好处:分类名称是稳定的键,前端或后续业务可以直接读取;同一分类的多条订单都留在 items 中,不会被后来的记录覆盖。

用 initial = [] 创建分组容器

把空数组作为第三个参数,是这类代码最重要的起点。它让第一轮回调拿到的 $carry 已经是可写的数组,也让没有数据时的返回值仍然是数组,而不是意外的 null。

 101, 'category' => 'book', 'amount' => 29.90],
    ['id' => 102, 'category' => 'tool', 'amount' => 49.00],
    ['id' => 103, 'category' => 'book', 'amount' => 15.50],
];

$grouped = array_reduce(
    $orders,
    function (array $carry, array $item): array {
        // 读取分组键;真实项目中应先约定缺失 category 的处理策略。
        $category = $item['category'];

        // 第一次遇到分类时创建桶,避免直接写入不存在的嵌套键。
        if (!isset($carry[$category])) {
            $carry[$category] = [
                'category' => $category,
                'items' => [],
                'total_amount' => 0.0,
            ];
        }

        // 重复分类进入同一个桶,不覆盖之前已经收集的订单。
        $carry[$category]['items'][] = $item;
        $carry[$category]['total_amount'] += $item['amount'];

        // 返回完整累加器,下一轮会把它作为新的 carry。
        return $carry;
    },
    [] // 显式 initial:空输入时也返回空数组。
);

print_r($grouped);
?>

这里的关键不是匿名函数本身,而是最后的 return $carry;。每次回调只返回当前订单,或者只返回当前分组,都会让上一轮已经积累的结果丢失。

新分组创建桶,已有分组追加记录

可以把回调想成一个很小的状态机:分类键不存在时创建状态,分类键已经存在时更新状态。上面的输入会得到两个分组,book 有两条订单,tool 有一条订单,总金额也只在对应桶内累加。

当前情况累加器动作结果变化
第一次遇到 book创建 book 桶items 增加第 1 条记录
第一次遇到 tool创建 tool 桶items 增加第 1 条记录
再次遇到 book复用已有桶items 追加第 2 条记录,金额继续累加

如果只想保留每组的计数,不需要把完整记录放入 items,也可以把桶缩小为 ['count' => 0]。先确定结果结构,再决定回调中保存哪些字段,通常比边写边改嵌套数组更不容易出错。

空数组、缺失字段和初始值要分开处理

PHP array_reduce 的 initial、空输入、新分组和已有分组追加边界说明图
图2:initial 与分组键边界说明图,强调空输入返回 initial、重复键追加以及回调必须返回累加器;这是静态说明图,不是运行截图。

官方手册说明,如果输入数组为空且没有传入 initial,array_reduce 会返回 null。所以接口约定希望始终返回数组时,应显式传入 []:

 $carry);

// 传入空数组:空输入仍得到可遍历的数组结果。
$withInitial = array_reduce([], fn (array $carry, array $item): array => $carry, []);

var_dump($withoutInitial); // NULL
var_dump($withInitial);    // array(0) { }
?>

另外,示例里直接读取 $item['category'],前提是输入契约保证该字段存在。若数据来自外部接口,可以明确选择跳过、归入 unknown,或抛出异常,不要让缺失字段在循环中悄悄生成错误分组。

最容易忽略的是“必须返回 carry”

回调可以改变数组变量,但 array_reduce 只把回调的返回值当成下一轮累加器。如果某个分支没有返回值,PHP 会得到 null,下一轮再按数组访问就可能出现警告或类型错误。

 0];
        $carry[$category]['count']++;

        // 所有路径都返回数组,避免下一轮 carry 变成 null。
        return $carry;
    },
    []
);
?>

什么时候适合用 array_reduce

当结果确实是“逐条输入累积成一个值”时,array_reduce 很清楚:计数、求和、按键聚合和构建摘要都适合。若回调里需要大量分支、多个外部副作用或复杂的异常恢复,普通 foreach 往往更直观;不要为了使用函数名而把状态逻辑隐藏在一个过长的闭包里。

我自己的判断是:分组逻辑不在于代码短,而在于累加器结构是否稳定。只要提前决定 initial、分组键和重复键的更新方式,array_reduce 就能把扁平记录整理成接口需要的结构;如果这些规则还没确定,先用普通循环把数据契约写清楚,维护成本通常更低。

相关问题

array_reduce 能同时拿到数组键吗?

标准回调只接收 $carry 和 $item,不会直接传入当前数组键。如果分组依据来自键名,先把键转换成记录字段,或使用更适合保留键的遍历方式。

为什么空输入得到 null?

因为没有任何一轮回调可以生成返回值;传入 initial 后,它既是首轮累加器,也是空输入时的最终结果。

回调里修改 carry 后还需要 return 吗?

需要。array_reduce 依赖回调返回值推进下一轮,所以每条路径都应返回完整累加器。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>