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。因此,分组场景中的累加器不应该是一个数字,而应该是一个可以不断添加分组的数组。

我现在会先写出结果结构,再写回调。假设接口需要下面这样的结果:
[
'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]。先确定结果结构,再决定回调中保存哪些字段,通常比边写边改嵌套数组更不容易出错。
空数组、缺失字段和初始值要分开处理

官方手册说明,如果输入数组为空且没有传入 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 依赖回调返回值推进下一轮,所以每条路径都应返回完整累加器。
-
371 收藏
-
303 收藏
-
347 收藏
-
112 收藏
-
387 收藏
-
368 收藏
-
375 收藏
-
382 收藏
-
229 收藏
-
111 收藏
-
477 收藏
-
312 收藏
-
350 收藏
-
491 收藏
-
241 收藏
-
498 收藏
-
501 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习