拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP8.5怎么使用array_reduce归约数组

PHP8.5怎么使用array_reduce归约数组

前言

先说一个必须澄清的事实:array_reduce()不是 PHP 8.5 的新函数,它从 PHP 4.0.5 起就存在了,在 PHP 8.5 里它的签名和行为都没有任何变化。标题里的版本号只是描述了运行环境,而不是这个函数的引入版本。本文按array_reduce()的真实行为来写,同时说明在 PHP 8.5 下它有什么新的写法(主要是管道操作符|>,PHP 8.5 引入)。

那为什么这个老函数这么容易写错?因为它有一条反直觉的默认行为,还有三个「看着像应该支持、其实并不支持」的能力:

Warning: Undefined array key "sku"

或者更隐蔽的——结果少了一半数据,但一行错误都没有。这类问题的根源是三个认知偏差:


  1. 不传第三个参数时,PHP 会把数组的第一个元素当作起始累加值,回调不会对第一个元素执行。

  2. 回调只能拿到值,拿不到键。想按sku做分组,得自己想办法把键带进去。

  3. $carry是按值传递的,不是引用,所以「在回调里直接改 carry」的写法不成立。


本文先讲清这三个行为,再给出五种典型归约的写法与更合适的替代函数,最后是一个完整可运行、可自测的示例。

一、真实签名与版本

array_reduce(array $array, callable $callback, mixed $initial = null): mixed
项目内容
引入版本PHP 4.0.5
PHP 8.5 的变化无(本文标题里的 8.5 是运行环境,不是引入版本)
返回值mixed,类型完全取决于回调返回什么
回调签名callback(mixed $carry, mixed $item): mixed
第三个参数$initial,可选,默认null
是否提供键否
$carry是否引用传递否

回调的两个参数含义:


  • $carry:上一轮回调的返回值;第一轮调用时它是$initial(有个重要例外,见下一节)。

  • $item:当前元素的值。注意是值,不是键值对。


二、不传 $initial 时的默认行为

这是array_reduce()最容易踩的一条:

<?php declare(strict_types=1); // 任何 PHP 版本(array_reduce 自 4.0.5 起存在) $nums = [2, 3, 5, 7]; $sum = array_reduce($nums, static function ($carry, $item) { echo "carry=" . var_export($carry, true) . ", item=$item\n"; return $carry + $item; }); echo "结果:$sum\n";

输出是:

carry=2, item=3 carry=5, item=5 carry=10, item=7 结果:17

注意第一行:carry不是null,而是数组的第一个元素2。也就是说回调只被调用了 3 次(元素个数减 1),第一个元素被当作初始值直接跳过了。

行为归纳如下:

情况返回值
数组为空、传了$initial$initial
数组为空、没传$initialnull
数组非空、传了$initial从$initial开始归约,回调对每个元素都执行
数组非空、没传$initial用第一个元素当归约起点,回调对第 2 个元素起执行

这个行为导致的典型故障是空数组返回null:调用方拿到它去做算术或foreach,在 PHP 8 里就是TypeError。

<?php declare(strict_types=1); // ❌ 空数组时返回 null,后面的计算直接炸 $total = array_reduce($items, static fn ($c, $i) => $c + $i['amount']); // ✅ 永远显式传初始值,空数组时返回 0 而不是 null $total = array_reduce($items, static fn ($c, $i) => $c + $i['amount'], 0);

结论很直接:写array_reduce()时永远传第三个参数。它同时解决了「空数组返回 null」和「第一个元素被跳过」两个问题。

三、五种典型归约,以及更该用的函数

array_reduce()能做的事情,有一大半有更专用、更快、更易读的写法。选择顺序应该是:专用函数 →foreach→array_reduce。

目标array_reduce()写法更推荐
求和array_reduce($a, fn ($c, $i) => $c + $i, 0)array_sum($a)
求积array_reduce($a, fn ($c, $i) => $c * $i, 1)array_product($a)
求最值array_reduce($a, fn ($c, $i) => max($c, $i), PHP_INT_MIN)max($a)/min($a)
计数array_reduce($a, fn ($c, $i) => $c + 1, 0)count($a)
连接字符串array_reduce($a, fn ($c, $i) => $c . $i, '')implode(',', $a)
判断全部满足array_reduce($a, fn ($c, $i) => $c && $i > 0, true)array_all($a, fn ($i) => $i > 0)(PHP 8.4+)
判断存在满足同上但用 `\\
找第一个匹配项归约成「找到就返回」array_find($a, fn ($i) => ...)(PHP 8.4+)
取第一个元素折腾一圈array_first($a)(PHP 8.5+)
分组(group by)适合foreach也行,看可读性
按 key 建索引适合array_column($a, null, 'id')(仅限单层数组)
展平嵌套数组可以,但注意复杂度array_merge(...$a)或foreach

真正留给array_reduce()的,是「把数组压成一个结构」这一类:分组、建索引、构建聚合对象。这些用foreach写反而更啰嗦(要先在循环外声明累加变量),而array_reduce()天然带一个「累加值」参数。

四、分组、建索引与展平该怎么写

分组(group by)

因为回调拿不到键,分组要靠元素自身携带的字段:

<?php declare(strict_types=1); // 需要 PHP 7.4+(箭头函数是 7.4 引入的) $orders = [ ['sku' => 'A-1', 'qty' => 2, 'price' => 19.9], ['sku' => 'B-2', 'qty' => 1, 'price' => 9.5], ['sku' => 'A-1', 'qty' => 3, 'price' => 19.9], ]; // 按 sku 分组 $grouped = array_reduce( $orders, static function (array $carry, array $order): array { $carry[$order['sku']][] = $order; return $carry; }, [] // 初始值必须是空数组,不能省 ); print_r(array_map('count', $grouped)); // [A-1 => 2, B-2 => 1]

按 key 建索引

<?php declare(strict_types=1); $users = [ ['id' => 7, 'name' => 'alice'], ['id' => 9, 'name' => 'bob'], ]; $byId = array_reduce( $users, static function (array $carry, array $user): array { $carry[$user['id']] = $user; return $carry; }, [] ); // [7 => [...], 9 => [...]]

同样的结果也能用array_column($users, null, 'id')一行搞定,而且更快——能用专用函数就别用归约。

展平:这里有个复杂度陷阱

<?php declare(strict_types=1); $chunks = [[1, 2], [3, 4], [5, 6]]; // ❌ 每一轮都 array_merge,累积数组被整体复制一次 $flat = array_reduce($chunks, static fn (array $c, array $i): array => array_merge($c, $i), []); // ✅ 直接展开参数,一次合并 $flat = array_merge(...$chunks); // ✅ 或者用 array_push 的变体逐段追加,总体是线性代价 $flat = array_reduce($chunks, static function (array $c, array $i): array { array_push($c, ...$i); return $c; }, []);

第一行的写法在数据量大时会明显变慢:array_merge()每次都要把「已经累积的全部元素」重新拷贝一遍,总代价是元素总数的平方级(O(n²)),而后面两种是线性级(O(n))。这不是 PHP 的 bug,是算法选择问题。

短路:array_reduce 做不到

array_reduce()会遍历全部元素,没有break的余地。想「一找到就停」,得换写法:PHP 8.4+ 可以直接用array_find($nums, static fn (int $n): bool => $n < 0),低版本就用foreach加break。

五、PHP 8.5 下更顺手的写法:管道操作符

PHP 8.5 引入的管道操作符(pipe operator)|>不会改变array_reduce()的行为,但它让「一串数据变换」读起来是从左到右的,配合归约很合适:

<?php declare(strict_types=1); // 需要 PHP 8.5+ $orders = [ ['amount' => 120.5, 'paid' => true], ['amount' => 80.0, 'paid' => false], ['amount' => 199.9, 'paid' => true], ]; // 已支付订单金额合计:筛选 → 取列 → 归约 $total = $orders |> (fn (array $o): array => array_filter($o, static fn (array $r): bool => $r['paid'])) |> (fn (array $o): array => array_column($o, 'amount')) |> (fn (array $a): float => array_reduce($a, static fn ($c, $i) => $c + $i, 0.0)); printf("合计:%.2f\n", $total); // 320.40

注意管道右侧的闭包都要用括号包起来——这是|>的语法要求(箭头函数会贪婪地吞掉后面的表达式)。另外,这一串的中间两步其实用array_sum()更直接,这里只是为了演示管道与归约的组合。

六、完整可运行示例

把下面这段存成reduce_demo.php直接运行(需要 PHP 8.0+,用到命名参数与match):

<?php declare(strict_types=1); // 需要 PHP 8.0+ $orders = [ ['sku' => 'A-1', 'qty' => 2, 'price' => 19.90, 'paid' => true], ['sku' => 'B-2', 'qty' => 1, 'price' => 9.50, 'paid' => false], ['sku' => 'A-1', 'qty' => 3, 'price' => 19.90, 'paid' => true], ['sku' => 'C-3', 'qty' => 5, 'price' => 3.00, 'paid' => true], ]; // 1) 空数组的两种返回:这是最容易踩的地方 var_dump(array_reduce([], static fn ($c, $i) => $c + 1)); // NULL var_dump(array_reduce([], static fn ($c, $i) => $c + 1, 0)); // int(0) // 2) 不传初始值时,第一个元素当起点,回调只跑 n-1 次 $calls = 0; $sum = array_reduce( [10, 20, 30], static function ($carry, $item) use (&$calls) { $calls++; return $carry + $item; } ); printf("不传初始值:sum=%d,回调执行 %d 次\n", $sum, $calls); // 60, 2 次 $calls = 0; $sum = array_reduce( [10, 20, 30], static function ($carry, $item) use (&$calls) { $calls++; return $carry + $item; }, 0 ); printf("传了初始值:sum=%d,回调执行 %d 次\n", $sum, $calls); // 60, 3 次 // 3) 按 sku 分组,并顺手算出每组金额 $bySku = array_reduce( $orders, static function (array $carry, array $o): array { $sku = $o['sku']; $carry[$sku]['count'] = ($carry[$sku]['count'] ?? 0) + 1; $carry[$sku]['amount'] = ($carry[$sku]['amount'] ?? 0.0) + $o['qty'] * $o['price']; $carry[$sku]['paid_qty'] = ($carry[$sku]['paid_qty'] ?? 0) + ($o['paid'] ? $o['qty'] : 0); return $carry; }, [] ); ksort($bySku); foreach ($bySku as $sku => $row) { printf("%-5s 订单数=%d 金额=%.2f 已付件数=%d\n", $sku, $row['count'], $row['amount'], $row['paid_qty']); } // 4) 用数组解构拿到「归约出的多个值」,避免多次遍历 [$totalAmount, $totalQty, $unpaidCount] = array_reduce( $orders, static function (array $carry, array $o): array { [$amount, $qty, $unpaid] = $carry; // PHP 7.1+ 的数组解构 $qty += $o['qty']; if ($o['paid']) { $amount += $o['qty'] * $o['price']; } else { $unpaid++; } return [$amount, $qty, $unpaid]; }, [0.0, 0, 0] ); printf("总金额=%.2f 总件数=%d 未付订单=%d\n", $totalAmount, $totalQty, $unpaidCount); // 5) 展平:对比两种写法的复杂度来源 $chunks = [[1, 2], [3, 4], [5, 6]]; $merge = array_reduce($chunks, static fn (array $c, array $i): array => array_merge($c, $i), []); $push = array_reduce($chunks, static function (array $c, array $i): array { array_push($c, ...$i); return $c; }, []); printf("array_merge 展平:%s\n", implode(',', $merge)); printf("array_push 展平:%s\n", implode(',', $push)); // 6) 性能自测:把这里跑一遍,用你自己机器上的数字说话 $big = range(1, 20000); $t0 = hrtime(true); $r1 = array_reduce($big, static fn ($c, $i) => $c + $i, 0); $t1 = hrtime(true); $r2 = array_sum($big); $t2 = hrtime(true); printf("array_reduce=%d 用时 %.3f ms;array_sum=%d 用时 %.3f ms\n", $r1, ($t1 - $t0) / 1e6, $r2, ($t2 - $t1) / 1e6);

最后一段是刻意留的:关于性能,不要相信任何博客(包括本文)里的数字,用这段代码在自己机器上跑一遍。你会看到array_sum()这类 C 实现的专用函数比逐元素 PHP 回调快不少——原因不是array_reduce()写得烂,而是每轮回调都是一次 PHP 函数调用。

常见坑点

1. 不传第三个参数,空数组时拿到null

❌$total = array_reduce($rows, fn ($c, $r) => $c + $r['amount']);—— 当$rows为空时返回null,后面拿去做算术或格式化就报TypeError。 ✅ 永远传初始值:array_reduce($rows, $fn, 0)。初始值还决定了累加值的类型。

2. 以为回调会对第一个元素执行

❌ 在回调里printf("处理第 %d 条\n", ++$n)统计处理条数,结果总是比数组长度少一。因为不传初始值时,第一个元素直接成了$carry。 ✅ 统计条数用count();确实需要回调处理每个元素,就传第三个参数。

3. 想在回调里用键

❌array_reduce($map, fn ($c, $item) => $c . $item['key'])—— 回调第二个参数是值不是[key, value],数组的键根本传不进来。 ✅ 用foreach ($map as $k => $v);或者先把键塞进元素里:array_map(fn ($k, $v) => [$k, $v], array_keys($map), $map)。

4. 想让$carry按引用修改

❌array_reduce($a, function (array &$carry, $item) { $carry[] = $item; })——$carry不是引用传递,加&声明只会得到一个警告或无效行为。 ✅ 老老实实return新的$carry。这也是为什么回调里的return绝对不能漏。

5. 回调里漏了return

❌array_reduce($a, function ($c, $i) { $c[] = $i; }, [])—— 少了return,本轮返回null,下一轮$carry就成了null,报错位置落在很远的下一轮。 ✅ 每个分支都要有return;用static fn ($c, $i) => ...箭头函数从语法上避免漏写。

6. 用array_reduce做「找到就停」

❌ 归约到「已经找到就不再看」,但回调仍会被执行 N 次,等于白跑;而且逻辑绕。 ✅ 用array_find()(PHP 8.4+)或foreach+break,它们天然短路。

7. 用array_merge在回调里累积大数组

❌fn ($c, $i) => array_merge($c, $i)—— 每轮都整体复制累积结果,总代价是 O(n²),数据量一大就卡住。 ✅ 用array_push($c, ...$i)追加,或直接array_merge(...$chunks)一次合并;复杂度降回 O(n)。

8. 用归约替代专用函数

❌array_reduce($a, fn ($c, $i) => $c + $i, 0)求和、array_reduce($a, fn ($c, $i) => $c . $i, '')连接字符串 —— 又慢又难读。 ✅array_sum($a)、implode(',', $a)、max($a)、count($a)都是 C 层实现,更快也更清楚。归约只留给「压成一个结构」的场景。

总结

问题结论
array_reduce()是 PHP 8.5 新函数吗不是,PHP 4.0.5 起就有,8.5 未改动它
签名array_reduce(array $array, callable $callback, mixed $initial = null): mixed
回调参数(mixed $carry, mixed $item),只有值没有键
不传第三个参数第一个元素成为起始$carry,回调只跑 n-1 次
空数组传了初始值返回初始值;没传返回null
$carry是引用吗不是,必须return新值
能短路吗不能,需要短路就用array_find()或foreach
什么时候该用分组、按 key 建索引、压成聚合结构
什么时候不该用求和/求积/最值/计数/连接字符串/取首元素,都有专用函数
PHP 8.5 的新写法用管道操作符 `\

array_reduce()的正确用法可以浓缩成一句话:永远显式传第三个参数,永远在回调里return。做到这两点,它就只是一个「带累加值的循环」;至于该不该用它,判断标准也很简单——如果 PHP 已经为你想做的事情提供了专用函数(array_sum、max、implode、array_column、array_find),那就用专用函数,把归约留给真正需要「压成一个结构」的场合。

返回列表