PHP 数组函数把常见的转换、筛选、聚合、合并和排序操作封装成标准 API;选择函数时,先看它如何处理键以及是否修改输入。
array_filter() 会保留旧键,array_merge() 会重排整数键,而 usort() 会原地修改数组并丢弃原键。
明确结果需要列表还是映射;为 array_reduce() 提供类型正确的初始值,并让排序比较函数返回负数、0 或正数。
是什么,为什么存在
PHP 的数组同时承担列表和有序映射两种角色。数组函数是在这种容器上执行常见操作的内置 API:array_map() 转换值,array_filter() 选择元素,array_reduce() 聚合状态,array_merge() 合并输入,array_keys() 提取键,usort() 按自定义顺序重排值。
这些函数解决的是重复遍历代码,而不是消除所有循环。函数名可以表达操作意图,PHP 也负责迭代细节;但键、空值和修改方式仍由各个 API 自己定义。只凭其他语言中 map、filter、reduce 的经验猜测 PHP 行为,很容易写出能运行却改变数据形状的代码。
array_map()、array_filter()、array_reduce() 和 usort() 接收 回调函数(callback) 。回调是作为参数传入另一函数、由后者在处理数据时调用的函数。箭头函数适合一个表达式的回调,逻辑包含多步校验或状态更新时,普通匿名函数通常更清楚。
六个函数最重要的区别不是语法,而是结果契约:
| 函数 | 主要操作 | 键行为 | 是否修改输入 |
|---|---|---|---|
array_map() | 把每个值转换为新值 | 单个输入数组时保留键 | 否 |
array_filter() | 保留满足条件的元素 | 保留键,可能留下空档 | 否 |
array_reduce() | 把所有值折叠为一个结果 | 不向回调传键 | 否 |
array_merge() | 按顺序合并数组 | 字符串键覆盖,整数键重排 | 否 |
array_keys() | 取得全部键或匹配值的键 | 返回从 0 开始的列表 | 否 |
usort() | 使用比较函数排序值 | 丢弃原键并重新编号 | 是 |
当输入只在内存中、操作边界明确时,这些函数很合适。需要提前终止、同时维护多份状态、处理流式数据,或必须保留复杂键关系时,foreach 或生成器往往更直接。数组函数不是可读性的自动保证;嵌套三四层回调通常已经超过它们的舒适区。
工作原理
这些函数都遍历数组,但遍历结果不同。理解它们时,应分别追踪值、键、回调参数、返回值和输入是否被修改,而不是只看最终元素内容。
array_map() 转换值
array_map($callback, $array) 为每个值调用一次回调,并用回调返回值组成新数组。只传一个输入数组时,新数组保留原键;传入两个或更多数组时,函数按位置组合对应值,并为结果创建连续整数键。
多个输入数组长度不一致时,较短数组缺少的位置会以 null 补齐。回调的参数数量应与输入数组数量匹配;如果回调要求的参数多于实际传入的数组,PHP 会抛出 ArgumentCountError。需要在回调中使用键时,可以显式组合 array_keys() 与 array_values(),也可以改用 foreach。
array_filter() 选择元素
array_filter($array, $callback) 保留回调结果转换为 true 的元素。默认模式只传值;ARRAY_FILTER_USE_KEY 只传键;ARRAY_FILTER_USE_BOTH 依次传值和键。无论采用哪种模式,返回数组都保留输入键。
省略回调时,array_filter() 会删除 PHP 认为“空”的值,其中包括 false、null、整数 0、字符串 "0"、空字符串和空数组。这种快捷写法只适合这些值确实都无效的领域。订单数量、表单输入或状态码中,0 往往是合法数据,应写出明确谓词。
array_reduce() 累积结果
array_reduce($array, $callback, $initial) 先把 $initial 作为累加器,再按数组迭代顺序把累加器和值传给回调。每次回调的返回值成为下一轮累加器,最后一次返回值就是整个函数的结果。键不会传入回调。
PHP 的行为与 JavaScript 不同:省略 $initial 并不会把首个元素当作初始值,而是从 null 开始处理第一个元素。空数组在没有初始值时也返回 null。求和使用 0,拼接字符串使用 '',构造映射使用 [];初始值同时定义空输入结果和累加器类型。
array_merge() 合并数组
array_merge(...$arrays) 从左到右处理输入。相同字符串键出现多次时,右侧值覆盖左侧值;整数键的值不会按键覆盖,而会追加到结果末尾,并从 0 开始重新编号。调用时不传参数会得到空数组。
数组联合运算符 + 使用另一套规则:它保留左侧已有的所有键,同键的右侧值被忽略,而且整数键不会重排。因此,array_merge() 适合拼接列表或让后方配置覆盖前方配置;+ 适合保留左侧键和值。两者都不会自动执行符合所有领域需求的深层配置合并。
array_keys() 提取键
array_keys($array) 按迭代顺序返回数组的所有整数键和字符串键。传入第二个参数后,它只返回值匹配 $filterValue 的键;第三个参数为 true 时使用 ===,否则使用宽松比较。
检查某个键是否存在时,应直接使用 array_key_exists(),而不是先创建完整键列表再调用 in_array()。isset($array[$key]) 的语义又不同:键存在但值为 null 时,它返回 false。选择哪个函数取决于你要检查“键存在”还是“值不为 null”。
usort() 定义顺序
usort(&$array, $callback) 原地排列值,把所有键替换为连续整数。 比较函数(comparator) 在左值应排在右值之前、相等或之后时,分别返回负整数、0 或正整数;<=> 正好符合这个契约。
PHP 8 起,比较结果相等的元素保留原有相对顺序,因此 usort() 是 稳定排序(stable sort) 。稳定性不会修复不一致的比较函数:如果回调对同一对值给出矛盾结果,或只返回布尔值,最终顺序仍不可信。PHP 8.2 起函数本身的返回类型是 true;有用结果在被修改的数组中。
示例
下面四个示例分别展示转换与筛选、归约、合并和排序。输出均由本地 PHP 8.3.33 运行得到。
筛选订单并转换显示值
订单以业务编号为键。筛选和映射都会保留这些键,所以第一次 JSON 编码得到对象;只有明确需要 JSON 列表时,才用 array_values() 重新编号。
<?php
$orders = [
104 => ['customer' => 'Mina', 'paid' => true, 'cents' => 1599],
207 => ['customer' => 'Omar', 'paid' => false, 'cents' => 2400],
310 => ['customer' => 'Liu', 'paid' => true, 'cents' => 800],
];
$paid = array_filter(
$orders,
fn(array $order): bool => $order['paid'],
);
$labels = array_map(
fn(array $order): string => sprintf('%s:%.2f', $order['customer'], $order['cents'] / 100),
$paid,
);
echo json_encode($labels, JSON_UNESCAPED_UNICODE), PHP_EOL;
echo json_encode(array_values($labels), JSON_UNESCAPED_UNICODE), PHP_EOL;{"104":"Mina:15.99","310":"Liu:8.00"}
["Mina:15.99","Liu:8.00"]$paid 中只剩键 104 和 310,$labels 继续保留这两个键。PHP 的 json_encode() 只把从 0 开始连续编号的数组编码为 JSON 数组,因此第一行使用对象形状。
第二行在序列化边界主动调用 array_values()。这种位置比在每一步都重排更容易审查:业务处理阶段保留订单编号,只有外部格式要求列表时才丢弃键。
按地区归约订单金额
array_reduce() 可以返回数组,而不只是数字。这里的空数组初始值既处理了空订单列表,也声明累加器是一张地区到金额的映射。
<?php
$orders = [
['region' => 'east', 'cents' => 1200],
['region' => 'west', 'cents' => 750],
['region' => 'east', 'cents' => 750],
];
$totals = array_reduce(
$orders,
function (array $carry, array $order): array {
$region = $order['region'];
$carry[$region] = ($carry[$region] ?? 0) + $order['cents'];
return $carry;
},
[],
);
foreach (array_keys($totals) as $region) {
printf("%s=%d\n", $region, $totals[$region]);
}east=1950
west=750回调每轮都返回更新后的 $carry。如果只修改局部变量却忘记返回,下一轮收到的累加器会变成 null,类型声明会很快暴露这个错误。
地区第一次出现的顺序决定了 $totals 的迭代顺序。array_keys() 取得地区名列表,但打印时仍通过原映射读取金额;它不会把金额复制进键列表。
区分合并与联合
用户设置放在 array_merge() 的右侧,因此覆盖默认设置。嵌套的 features 数组会整体替换;这个函数只处理顶层键。
<?php
$defaults = [
'theme' => 'light',
'page_size' => 20,
'features' => ['search'],
];
$user = ['theme' => 'dark', 'features' => ['export']];
$settings = array_merge($defaults, $user);
$states = [10 => 'draft', 20 => 'review']
+ [20 => 'ignored', 30 => 'published'];
echo json_encode($settings), PHP_EOL;
echo json_encode($states), PHP_EOL;{"theme":"dark","page_size":20,"features":["export"]}
{"10":"draft","20":"review","30":"published"}第二个表达式使用 +,所以左侧键 20 的 review 胜出,整数键 10、20、30 保持不变。因为这些键不是连续列表索引,JSON 结果仍是对象。
如果把两个操作互换,代码仍然合法,但数据政策完全改变。配置合并代码应通过变量名或测试说明覆盖方向,而不能只依赖参数位置让读者猜测。
复制后稳定排序
usort() 会修改参数,因此先复制 $tasks。两个优先级为 1 的任务被比较为相等,在 PHP 8.3 中保持输入顺序。
<?php
$tasks = [
['name' => 'write', 'priority' => 2],
['name' => 'test', 'priority' => 1],
['name' => 'deploy', 'priority' => 1],
];
$sorted = $tasks;
usort(
$sorted,
fn(array $left, array $right): int =>
$left['priority'] <=> $right['priority'],
);
echo implode(',', array_column($sorted, 'name')), PHP_EOL;
echo implode(',', array_column($tasks, 'name')), PHP_EOL;test,deploy,write
write,test,deploy第一行证明排序结果按优先级升序排列,也证明相等项 test 和 deploy 的相对顺序没变。第二行证明原数组仍保持原顺序,因为传给 usort() 的是 PHP 数组的副本。
如果业务要求优先级相同时再按名称排序,就应把第二比较条件写进回调。稳定排序只保留相等项的输入顺序,不会凭空产生业务上的决胜规则。
陷阱
数组函数最危险的错误通常不会抛异常,而是悄悄改变键或把合法值当成空值。测试应同时断言值、键顺序和输入是否被修改。
过滤后把映射当列表
修复: 先决定结果是业务映射还是列表。只有消费者要求连续索引时才调用 array_values();如果键携带订单号或数据库 ID,就应保留它们,并让序列化格式明确表达映射。
用默认过滤误删 0
修复: 用领域条件写回调,例如 fn($value) => $value !== null。不要用 array_filter($values) 代替输入校验,因为 PHP 的空值集合通常比业务上的无效值集合更大。
误以为首项是归约初始值
修复: 总是为可能为空的输入写出单位值,并让类型与结果一致。省略求积的初始值会让 null 参与运算,结果不再是你以为的首项起步;使用 1 才能表达乘法单位元。
把浅层合并当成配置策略
修复: 先定义每个配置节点是覆盖、追加还是递归替换,再选择 API 或编写小型领域函数。至少测试重复字符串键、整数键和嵌套数组,不要把“recursive”理解为“符合配置覆盖需求”。
颠倒回调参数或返回类型
修复: 给回调参数写出领域名称和类型,并用一组小于、相等、大于的输入验证比较符号。布尔值转换为整数后只有 0 和 1,无法同时表达“排在前面”和“相等”。
忘记排序会修改输入
修复: 需要同时保留原顺序时先赋值到新变量;需要保留键值关联时考虑 uasort()。不要把 usort() 的 true 返回值赋给 $sorted,真正的排序结果仍在传入变量里。
边界语义
六个函数组合使用时,边界行为比单个函数的主路径更值得检查。尤其要关注输入数量、回调声明、PHP 键转换和排序相等项。
多数组映射改变键契约
单数组 array_map() 保留键,多数组形式却按位置对齐并返回连续整数键。即使两个输入都使用相同字符串键,函数也不会按键关联它们;它只使用各自的迭代位置。
较短输入会在尾部以 null 补齐,这会让声明为非空类型的回调在运行时抛出 TypeError。需要按业务键连接两张映射时,应显式遍历键并检查缺失项,而不是把多数组 array_map() 当作 join。
回调只看到 API 承诺的数据
array_reduce() 只传累加器和值,不传当前键。为了按键归约而先调用 array_keys() 是可行的,但回调随后需要通过闭包读取原数组;逻辑复杂时,foreach ($array as $key => $value) 更清楚。
array_filter() 的模式只改变回调参数,不改变返回数组的键保留规则。ARRAY_FILTER_USE_KEY 下回调看不到值,ARRAY_FILTER_USE_BOTH 下值在前、键在后;类型声明能让颠倒参数更早失败。
PHP 键决定 JSON 形状
PHP 没有独立的列表运行时类型;列表只是键为 0 到 n - 1 的数组。筛选会留下空档,联合会保留非连续整数键,而 array_merge() 与 usort() 会重新编号。相同的值集合可能因此产生不同 JSON 形状。
不要为了得到 JSON 数组就盲目丢弃键。先确认键是否是业务标识;如果是,应把它写成对象字段,或有意输出 JSON 对象。序列化测试需要断言完整字符串或解码后的结构,而不只是值是否出现。
稳定排序仍需要完整规则
PHP 8 的稳定性只约束比较函数返回 0 的元素。输入本身来自不确定顺序的数据源时,保留输入相对顺序并不能得到跨运行稳定的业务结果;需要确定性输出时,应加入明确的第二排序字段。
比较函数还应满足一致性:比较同一对值时结果不能随外部可变状态变化,compare(a, b) 与 compare(b, a) 的符号应相反。不要在回调里执行网络请求、修改被比较元素或读取会变化的时钟。
值查找的严格性
array_keys($array, $value) 默认采用宽松比较,因此不同类型的值可能被视为匹配。数据经过表单、JSON 或数据库驱动后,数字与数字字符串混在一起很常见;如果类型本身有意义,第三个参数应传 true。
严格查找不会把键也变成严格类型。PHP 数组键本来就只能是整数或字符串,并且某些合法十进制整数字符串在存入数组时会转换为整数键。检查值的比较方式与理解键的规范化是两件不同的事。
数组函数与显式循环的取舍
一次转换或筛选通常适合标准函数,聚合状态也可以自然地写成归约。若管道需要多次遍历同一大数组、在找到结果后立刻停止,或同时依赖键和值并处理错误,显式循环常常更短,也更容易逐步调试。
可读性的判断标准是数据政策是否显而易见。一个命名良好的 foreach 不比嵌套的 array_map(array_filter(...)) 低级;选择能让键、空输入和修改边界最容易审查的形式。
组合操作的审查方法
数组管道把多个正确的函数连起来后,仍可能产生错误结果。审查时不要只验证最后几个值,还要逐步写下中间数组的键、类型和所有权。
一个实用办法是给每一步准备最小反例,而不是只用整齐的连续列表。加入缺失键、合法零值和重复键后,错误的默认假设通常会立刻显现。
尽量晚重排键
array_values() 会永久丢掉原键,后续步骤无法恢复订单编号或来源位置。把重新编号留到确实要求列表的输出边界,前面的诊断信息会更多。
如果后续必须用位置访问元素,应在变量名中写明形状,例如 $paidOrderList。这个名称让读者知道键已经不再承载业务意义,也能阻止后续代码误把位置当作 ID。
明确定义空输入
每个聚合都应有可解释的空结果:金额总和是 0,分组结果是 [],拼接文本可能是 ''。把它作为 array_reduce() 的初始值,正常输入和空输入就共享同一返回类型。
如果不存在合理的空结果,先检查输入并抛出领域异常通常比返回 null 更清楚。不要让省略初始值替你偷偷选择 null;那只是 API 默认值,不是业务决策。
区分转换与校验
array_map() 适合把每个有效输入转换为输出,array_filter() 适合按已定义的条件选择输入。把解析失败、缺字段和授权失败都折叠成 null,再用无回调过滤,会丢失错误原因,也可能误删合法零值。
需要报告每个失败时,可以用显式循环收集结果和错误,或让回调抛出有上下文的异常。调用方应能区分“没有匹配项”与“输入无法处理”。
先命名复杂步骤
连续嵌套的数组函数按照从内到外的顺序执行,而代码阅读通常从外向内开始。把重要的中间结果命名为 $eligibleOrders、$totalsByRegion 之类的变量,可以显露数据形状和政策。
命名步骤也方便在调试器中检查键以及为单步行为写测试。若一个回调需要捕获多个变量并包含几个分支,把它提取成命名函数或普通循环通常更省事。
用形状断言保护边界
测试列表结果时,既断言 array_is_list($result),也断言元素顺序。测试映射时则断言确切键及其类型,避免 array_values() 或 array_merge() 的重排在重构中悄悄通过。
排序测试至少包含一对相等项,并保留排序前数组用于比较。合并测试至少包含一次字符串键冲突、一次整数键冲突和一个嵌套数组;这几类输入能直接暴露函数选错的问题。
延伸阅读
5个问题 · 1 道输出预测题 · 1 道找错题