一、为什么需要递归版本
array_replace()处理单层数组没问题,一旦数组嵌套,它会把整个子数组当成一个值整体替换掉。实际项目里的配置、表单数据、API响应结构,往往是多层嵌套的,只想改其中某一个叶子节点,不想把整棵子树都换掉。
array_replace_recursive()就是解决这个问题的。它沿着键名往下走,遇到两边都是数组的键,继续递归;遇到不是数组的值,才执行覆盖。这样改哪一层就只动哪一层。
二、函数原型与参数
array_replace_recursive(array $array1, array ...$replacements): array
| 参数 | 说明 | 是否必需 |
|---|---|---|
| array1 | 被替换的基准数组 | 必需 |
| replacements | 一个或多个替换数组 | 可选 |
返回值:替换后的新数组。参数中存在非数组时返回null。原数组不受影响。
替换数组可以有多个,后面的数组优先级更高,会覆盖前面数组在同一路径上的值。
三、递归替换的判定规则
核心逻辑可以用三步概括:
-
遍历替换数组的每个键。
-
如果基准数组和替换数组在同一个键上都是数组,进入下一层递归。
-
否则,用替换数组的值覆盖基准数组同键的值;键不存在则追加。
这里的关键是第二条。两边的值必须同时是数组才会递归,只要有一边不是数组,就直接覆盖。看例子:
<?php
$array1 = array("a" => array("apple"), "b" => array("ball"));
$array2 = array("a" => array("all"), "b" => array("black"));
print_r(array_replace_recursive($array1, $array2));
输出:
Array
(
[a] => Array
(
[0] => all
)
[b] => Array
(
[0] => black
)
)
键a和b在两边都是数组,进入递归。子数组里键0对应的值不是数组,执行覆盖,所以apple变成all,ball变成black。
四、长度不等的子数组怎么处理
这是array_replace_recursive()里值得单独拿出来讲的地方。递归到子数组后,数字键的处理是"同键覆盖、无键保留原值"。
<?php
$color1 = array("a" => array("blue"), "b" => array("green", "red"));
$color2 = array("a" => array("yellow"), "b" => array("black"));
$color3 = array("a" => array("saffron"), "b" => array("pink"));
print_r(array_replace_recursive($color1, $color2, $color3));
输出:
Array
(
[a] => Array
(
[0] => saffron
)
[b] => Array
(
[0] => pink
[1] => red
)
)
拆开看键b的变化:
-
$color1的b是["green","red"],下标0和1。 -
$color2的b是["black"],只有下标0,把green覆盖成black,下标1的red保留。 -
$color3的b是["pink"],把下标0的black覆盖成pink,下标1的red依然保留。
最终b是["pink","red"]。替换数组的元素个数少于原数组时,原数组多出来的元素不会被删除,会保留下来。这一点和array_replace()单层行为一致,但嵌套时容易被忽略。
再看一个类似例子:
<?php
$player1 = array("a" => array("sachin"), "b" => array("ganguly", "sehwag"));
$player2 = array("a" => array("yuvraj"), "b" => array("ishant"));
print_r(array_replace_recursive($player1, $player2));
输出:
Array
(
[a] => Array
(
[0] => yuvraj
)
[b] => Array
(
[0] => ishant
[1] => sehwag
)
)
b的0号被ishant覆盖,1号的sehwag因为替换数组没有该下标,保留下来。
五、与相关函数的差异
| 函数 | 递归 | 数字键处理 | 字符串键处理 | 适用场景 |
|---|---|---|---|---|
| array_replace() | 否 | 同键覆盖,不重排 | 同键覆盖 | 单层数组替换 |
| array_replace_recursive() | 是 | 子数组内同键覆盖,不重排 | 逐层覆盖 | 多层配置合并 |
| array_merge_recursive() | 是 | 同键合并成数组 | 同键合并成数组 | 需要保留双方值 |
| array_merge() | 否 | 重新编号追加 | 同键覆盖 | 列表合并 |
要特别注意array_replace_recursive()和array_merge_recursive()的区别。前者是"覆盖",后者是"合并"。如果两边同一个键都是数组,array_merge_recursive()会把两个子数组的元素拼在一起,而不是替换。配置覆盖场景要的是替换,用array_replace_recursive()才对。
个人经验:早期做一个后台主题配置功能,默认主题色和用户自定义色都是嵌套数组。本来想用array_merge_recursive()合并,结果用户只改了按钮颜色,两个颜色值被合并成了一个数组,前端拿到["#fff","#000"]这种结构直接渲染异常。换成array_replace_recursive()后,用户改哪个叶子节点就覆盖哪个,其余保持默认。"合并"和"覆盖"是两种语义,配置场景几乎都是覆盖。
六、项目实例
实例一:代码号学习编程·多层配置逐项覆盖
<?php
$default = [
'db' => [
'host' => 'localhost',
'port' => 3306,
'charset' => 'utf8mb4',
],
'cache' => [
'driver' => 'file',
'ttl' => 3600,
],
];
$userConfig = [
'db' => [
'host' => '192.168.1.10',
'port' => 3307,
],
];
print_r(array_replace_recursive($default, $userConfig));
输出:
Array
(
[db] => Array
(
[host] => 192.168.1.10
[port] => 3307
[charset] => utf8mb4
)
[cache] => Array
(
[driver] => file
[ttl] => 3600
)
)
用户只改了host和port,charset保留默认,cache整块不动。这正是递归替换想要的效果。
实例二:代码号学习编程·多语言文案分层覆盖
<?php
$lang1 = array("a" => array("C"), "b" => array("Java", "PHP"));
$lang2 = array("a" => array("Perl"), "b" => array("HTML"));
print_r(array_replace_recursive($lang1, $lang2));
输出:
Array
(
[a] => Array
(
[0] => Perl
)
[b] => Array
(
[0] => HTML
[1] => PHP
)
)
a组的下标0被Perl覆盖,b组的下标0被HTML覆盖,下标1的PHP保留。这个行为在覆盖默认语言包时很常见:只替换翻译过的条目,没翻译的继续用默认值。
实例三:代码号学习编程·表单默认值与提交值合并
<?php
$formDefault = [
'profile' => [
'nickname' => '',
'city' => '',
'tags' => ['php', 'mysql'],
],
];
$formInput = [
'profile' => [
'nickname' => '代码号',
'tags' => ['redis'],
],
];
print_r(array_replace_recursive($formDefault, $formInput));
输出:
Array
(
[profile] => Array
(
[nickname] => 代码号
[city] =>
[tags] => Array
(
[0] => redis
[1] => mysql
)
)
)
tags数组里0号被覆盖,1号保留。这里要留意:如果业务希望tags以用户提交为准,递归替换反而不合适,因为残留的mysql不是用户想要的结果。这种"数组整体替换"的需求,单层array_replace()或直接赋值更稳妥。
七、踩坑记录
坑一:数字子数组残留元素。替换数组的子数组比原数组短时,原数组多出来的元素不会被清掉。列表语义的数据用递归替换,会拿到意料之外的尾部元素。
坑二:和array_merge_recursive()混用。想要覆盖结果却用了合并函数,同键数组被拼在一起,结构变了。认准"覆盖"这个语义。
坑三:以为能删除键。array_replace_recursive()只做替换和追加,不能通过传空值来删除某个键。要删除键得用unset或array_diff_key。
坑四:参数顺序写反。array_replace_recursive($override,$base)和array_replace_recursive($base,$override)效果相反。基准数组放第一个,覆盖数组放后面。
坑五:传入非数组参数。有非数组参数时返回null,后续操作null会出错。调用前应做类型检查。
坑六:深层递归的性能。嵌套层数很深、数组规模很大时,递归开销会体现出来。配置合并这类低频操作没问题,高频循环里应避免反复递归替换。
八、本节课程知识要点
-
array_replace_recursive()沿键名逐层递归,两边同为数组才继续往下,否则覆盖。
-
子数组内数字键同键覆盖,替换数组没有的下标保留原值,不会删除。
-
多个替换数组时,靠后的优先级更高。
-
与array_merge_recursive()语义不同,前者是覆盖,后者是合并。
-
返回新数组,原数组不变;参数非法时返回null。
-
列表语义的子数组不适合递归替换,容易残留尾部元素。
九、常见问题
问:它和array_replace()最根本的区别是什么?
array_replace()只处理一层,遇到子数组整体替换;array_replace_recursive()会进入子数组继续按键替换。
问:子数组里的元素会被删掉吗?
不会。替换数组没有覆盖到的下标,原值保留。
问:能不能用它删除某个键?
不能。它只做替换和追加。删除键要用unset()或array_diff_key()。
问:替换数组里的值是null会怎样?
null不是数组,会直接覆盖对应键的值,键变成null,不会触发递归。
问:多层配置合并该选哪个?
要"用户改了才覆盖、没改保留默认",用array_replace_recursive()。要"两边值都保留",才考虑array_merge_recursive()。