一、array_reduce()的定位
数组处理函数大致分两类:一类是"变换",比如array_map把每个元素改成另一个值;一类是"筛选",比如array_filter保留符合条件的元素。array_reduce()属于第三类——归约,把整个数组压缩成一个结果。
这个结果不一定是个数字。累加、累乘、字符串拼接、构造嵌套数组,都可以用它完成。理解"归约"这个概念,比记住函数签名更重要。
二、函数原型与参数
array_reduce(array $array, callable $callback, mixed $initial = null): mixed
| 参数 | 说明 | 是否必需 |
|---|---|---|
| array | 待归约的数组 | 必需 |
| callback | 用户自定义的回调函数,接收两个参数 | 必需 |
| initial | 初始值,作为第一次回调的第一个参数 | 可选 |
返回值:归约后的最终结果,类型由回调的返回类型决定,可以是int、float、string、array等。
回调函数的签名是callback(mixed$carry,mixed$item):mixed。这里有两个关键点:
第一,参数顺序。第一个参数是"累计值"(carry),第二个参数是"当前元素"。很多人第一次写会记反,把元素当成第一个参数,结果怎么算都不对。
第二,初始值的作用。如果不传initial,PHP会把数组的第一个元素当作初始carry,然后从第二个元素开始遍历。如果传了initial,则从第一个元素开始,initial作为第一次回调的carry。
三、初始值对结果的影响
这是array_reduce()最容易出问题的地方。看两个对比:
不传初始值:
<?php
function add($carry, $item) {
$carry += $item;
return $carry;
}
$nums = array(1, 2, 3, 4, 5, 6);
echo array_reduce($nums, "add");
输出:
21
传初始值10:
<?php
function add($carry, $item) {
$carry += $item;
return $carry;
}
$nums = array(1, 2, 3, 4, 5, 6);
echo array_reduce($nums, "add", 10);
输出:
31
初始值直接参与运算。做累加时初始值传0、做累乘时传1、做字符串拼接时传空串,是常见做法。但要注意,传了初始值后,数组的第一个元素也会进入回调,不会因为"已经有初始值"而被跳过。
四、回调里能做的事
回调的返回类型决定了整个归约的方向。几个典型场景:
字符串拼接:
<?php
function joinWithDash($carry, $item) {
return $carry . "-" . $item;
}
$langs = array("PHP", "java", "python");
print_r(array_reduce($langs, "joinWithDash", "code"));
输出:
code-PHP-java-python
初始值"code"作为第一段,后面依次拼接。这种写法适合构造带前缀的字符串。
累乘:
<?php
function multiply($carry, $item) {
$carry = $carry * $item;
return $carry;
}
$nums = array(2, 4, 5, 10, 100);
print_r(array_reduce($nums, "multiply", 3));
输出:
120000
初始值3与所有元素相乘。如果初始值传1,结果就是元素本身的乘积。传0的话,整个结果会变成0,这是累乘场景下要避开的一个坑。
归约成数组:
回调不一定返回标量。把元素按条件分组,最终返回一个数组,同样可行:
<?php
$users = [
['name' => 'A', 'age' => 20],
['name' => 'B', 'age' => 30],
['name' => 'C', 'age' => 20],
];
$grouped = array_reduce($users, function ($carry, $user) {
$carry[$user['age']][] = $user['name'];
return $carry;
}, []);
print_r($grouped);
输出:
Array
(
[20] => Array
(
[0] => A
[1] => C
)
[30] => Array
(
[0] => B
)
)
这里初始值传空数组[]是必需的。如果不传,第一次回调的carry会变成第一个用户数组,逻辑就乱了。
五、与foreach、array_sum的取舍
| 方式 | 适用场景 | 可读性 | 性能 |
|---|---|---|---|
| array_reduce() | 归约逻辑固定、想用函数式表达 | 高(逻辑集中) | 略低(回调开销) |
| foreach | 逻辑复杂、需要提前break | 中 | 高 |
| array_sum() | 纯数字求和 | 高 | 高 |
| array_product() | 纯数字求积 | 高 | 高 |
选择原则:
-
单纯求和用array_sum(),不要用array_reduce()绕一圈。
-
单纯求积用array_product()。
-
归约逻辑带条件判断、类型转换时,array_reduce()能把逻辑收在一个回调里,可读性好。
-
需要在遍历中途break或做副作用操作时,foreach更合适。
个人经验:曾经在一个统计脚本里,用array_reduce()做"按状态累加订单金额",写法很紧凑。后来需求变成"累加过程中如果遇到异常状态就中断并记录日志",array_reduce()就不合适了,因为回调里没法直接中断整个归约。归约适合"必须把整个数组跑完"的场景,需要提前退出的还是foreach。
六、项目实例
实例一:代码号学习编程·计算购物车总价
<?php
$items = [
['price' => 99, 'qty' => 2],
['price' => 45, 'qty' => 1],
['price' => 120, 'qty' => 3],
];
$total = array_reduce($items, function ($carry, $item) {
return $carry + $item['price'] * $item['qty'];
}, 0);
echo $total; // 603
初始值传0,保证累加的起点是数字,不会因数组为空而返回null。
实例二:代码号学习编程·拼接SQL条件片段
<?php
$conditions = ['status = 1', 'deleted = 0', 'uid > 0'];
$where = array_reduce($conditions, function ($carry, $item) {
return $carry === '' ? $item : $carry . ' AND ' . $item;
}, '');
echo $where;
输出:
status = 1 AND deleted = 0 AND uid > 0
这里用初始值空串配合判断,避免了第一个元素前面多出一个AND。
实例三:代码号学习编程·数组按键分组
<?php
$logs = [
['level' => 'error', 'msg' => 'A'],
['level' => 'info', 'msg' => 'B'],
['level' => 'error', 'msg' => 'C'],
];
$byLevel = array_reduce($logs, function ($carry, $log) {
$carry[$log['level']][] = $log['msg'];
return $carry;
}, []);
print_r($byLevel);
输出:
Array
(
[error] => Array
(
[0] => A
[1] => C
)
[info] => Array
(
[0] => B
)
)
分组类需求用array_reduce()比先array_filter再合并更直接。
七、踩坑记录
坑一:回调参数顺序写反。第一个是carry,第二个是item。写反后累加结果会错乱,且不容易一眼看出来。
坑二:累乘初始值传0。0乘任何数都是0,整个归约结果直接归零。累乘初始值应传1。
坑三:归约成数组时不传初始值。不传时第一次carry是第一个元素(可能是关联数组),后续往它里面塞数据,结构会和预期不同。归约成数组、字符串时应显式传初始值。
坑四:以为空数组会返回初始值。空数组配初始值时确实返回初始值;但空数组不传初始值时返回null。业务里如果依赖返回值类型,要显式传初始值。
坑五:在回调里做副作用。往数据库写、打日志这类操作放进回调,会让归约逻辑变得难以测试。回调保持纯粹,副作用放到外层。
八、本节课程知识要点
-
array_reduce()把数组归约为单个值,返回类型由回调决定。
-
回调第一个参数是累计值carry,第二个是当前元素item。
-
不传初始值时,第一个元素充当初始carry;传了则从第一个元素开始回调。
-
累加初始值传0,累乘初始值传1,拼接字符串传空串,归约成数组传空数组。
-
纯求和用array_sum(),纯求积用array_product(),不要用array_reduce()绕路。
-
需要中途中断遍历时,array_reduce()不合适,改用foreach。
九、常见问题
问:array_reduce()能处理关联数组吗?
可以。它遍历的是值,键名不会传给回调。如果回调里需要键名,得先用array_keys或其他方式把键带进元素里。
问:回调能不能接收第三个参数?
不能。array_reduce()的回调只接收两个参数。需要额外上下文时,用use关键字把外部变量引入闭包。
问:初始值传null和不传有区别吗?
有。不传时初始carry是数组第一个元素;显式传null时,第一次回调的carry是null。两者行为不同,别混用。
问:归约结果类型和数组元素类型不一致可以吗?
可以。回调返回什么类型,最终结果就是什么类型。数组元素是数字,归约成字符串也没问题。