一、array_udiff_assoc的定位
array_udiff_assoc()用来计算多个数组的差集:返回第一个数组中存在、但其他数组里不存在的元素。它有两个比较维度——键名和值,但两者的比较方式不同:
-
键名:用PHP内置规则比较,也就是普通的
==判断,不做类型转换之外的处理; -
值:用用户自定义的回调函数比较。
这个函数从PHP5.0开始提供。名字拆开看更好理解:array表示数组操作,u表示用户自定义函数,diff表示求差集,assoc表示比较时考虑键名关联。合起来就是“带键名比较、值用自定义规则”的差集函数。
语法:
array_udiff_assoc(array $array1, array $array2, [, array $...], callable $value_compare_func): array
参数说明:
| 参数 | 说明 | 是否必填 |
|---|---|---|
| array1 | 被比较的主数组 | 必填 |
| array2 | 与第一个数组比较的数组 | 必填 |
| array3... | 继续参与比较的数组 | 可选 |
| value_compare_func | 用户自定义的值比较函数 | 必填 |
这里有一个原始资料中值得纠正的地方:参数表把array2标为必填是对的,但把value_compare_func写在参数列表末尾、又标为必填,容易让人误以为它是可选参数。实际调用时,回调函数必须提供,否则函数无法确定值的比较规则。回调函数在语法签名中位于之后,但它是必填项,这一点在写代码时要特别注意。
返回值:返回一个数组,包含array1中那些在其他数组里找不到对应键值对的元素。键名会保留。
二、比较逻辑的两个层面
理解array_udiff_assoc()的关键,是把它拆成两层来看。
第一层:键名比较
键名比较用的是PHP内置规则。也就是说,"a"和"a"视为同一个键,1和"1"在关联数组键名的处理上也归为同一个键。只有键名相同,两个元素才有资格进入值比较环节。
第二层:值比较
值比较交给用户自定义函数。回调接收两个值$x和$y,返回值规则和usort()、uasort()一致:
-
$x===$y或$x==$y时返回0,表示两个值相等; -
$x>$y返回1; -
$x<$y返回-1。
需要说明的是,array_udiff_assoc()只关心回调返回的是不是0。返回0就认为值相等,非0就认为值不相等。所以示例里有些函数写成return1;,有些写成return($x>$y)?1:-1;,在差集判断的结果上是一样的。不过从代码规范角度看,返回-1、0、1三态更清晰,也方便复用到其他排序函数。
三、代码号学习编程实例
示例1:笔记本品牌差集
<?php
function compareLaptop($x, $y)
{
if ($x === $y) {
return 0;
}
return ($x > $y) ? 1 : -1;
}
$lapTop1 = ["a" => "dell", "b" => "hp", "c" => "asus"];
$laptop2 = ["a" => "dell", "b" => "asus", "c" => "hp"];
$result = array_udiff_assoc($lapTop1, $laptop2, "compareLaptop");
print_r($result);
输出:
Array
(
[b] => hp
[c] => asus
)
分析一下:键a在两个数组里都是dell,值相等,被排除。键b在第一个数组里是hp,第二个数组里是asus,值不同,保留。键c在第一个数组里是asus,第二个数组里是hp,值不同,保留。所以结果是b=>hp和c=>asus。
示例2:球类项目差集
<?php
function compareGame($a, $b)
{
if ($a === $b) {
return 0;
}
return 1;
}
$game1 = ["a" => "cricket", "b" => "cricket", "c" => "football"];
$game2 = ["a" => "cricket", "b" => "football", "c" => "cricket"];
print_r(array_udiff_assoc($game1, $game2, "compareGame"));
输出:
Array
(
[b] => cricket
[c] => football
)
这个示例说明了一个容易忽略的点:值相同但键不同的元素,不会被当成同一个元素。$game1["b"]是cricket,$game2["b"]是football,键相同、值不同,保留。$game1["c"]是football,$game2["c"]是cricket,同样保留。如果只按值比较、不看键,结果会不同,那是array_udiff()的职责。
示例3:板球运动员名单差集
<?php
function comparePlayer($y, $z)
{
return ($y === $z) ? 0 : 1;
}
$array1 = [
"a" => "sachin",
"b" => "sehwag",
"d" => "rahul",
"r" => "virat"
];
$array2 = [
"a" => "sachin",
"y" => "sehwag",
"d" => "rahul",
"x" => "yuvraj"
];
$compare = array_udiff_assoc($array1, $array2, "comparePlayer");
print_r($compare);
输出:
Array
(
[b] => sehwag
[r] => virat
)
键a和d在两个数组里键名、值都一致,被排除。键b在$array2中不存在,保留。键r在$array2中也不存在,保留。键y和x只存在于$array2,不会出现在结果里,因为差集的方向是“从$array1出发”。
示例4:编程语言差集
<?php
function compareLang($a, $b)
{
return ($a == $b) ? 0 : 1;
}
$a1 = ["a" => "php", "b" => "java", "c" => "python"];
$a2 = ["a" => "php", "b" => "python", "c" => "java"];
print_r(array_udiff_assoc($a1, $a2, "compareLang"));
输出:
Array
(
[b] => java
[c] => python
)
这里回调用的是==而不是===。对于字符串值,两者结果一致;但如果值里混有数字和数字字符串,==会把"1"和1视为相等,而===不会。选择哪种,取决于业务上是否允许类型宽松比较。
四、和几个相似函数的区别
PHP里求差集的函数不止一个,名字也接近,放在一起对比更清楚:
| 函数 | 键名比较方式 | 值比较方式 |
|---|---|---|
array_diff_assoc() |
内置规则 | 内置规则 |
array_udiff_assoc() |
内置规则 | 用户自定义 |
array_udiff() |
不比较键名 | 用户自定义 |
array_udiff_uassoc() |
用户自定义 | 用户自定义 |
选择思路:
-
键名和值都用默认规则,用
array_diff_assoc(); -
键名用默认规则、值要自定义,用
array_udiff_assoc(); -
只看值、不看键名,用
array_udiff(); -
键名和值都要自定义,用
array_udiff_uassoc()。
我在项目里见过一种情况:同事想按自定义规则比较对象数组,直接用了array_udiff_assoc(),结果发现键名对不上,差集算出来是空数组。原因是他以为这个函数会忽略键名,实际上它恰恰是“带键名比较”的版本。键名不一致的元素,连进入值比较的机会都没有。这个坑值得记一下。
五、项目实战经验
1.配置项差异对比
做多环境配置同步时,经常要找出“测试环境有、生产环境没有”或者“值不一致”的配置项。配置数组通常是关联数组,键是配置名,值是配置内容,正好适合array_udiff_assoc():
<?php
$testConfig = [
"db_host" => "127.0.0.1",
"db_port" => "3306",
"cache" => "redis"
];
$prodConfig = [
"db_host" => "10.0.0.5",
"db_port" => "3306",
"cache" => "redis"
];
$diff = array_udiff_assoc($testConfig, $prodConfig, function ($a, $b) {
return $a === $b ? 0 : 1;
});
print_r($diff);
输出:
Array
(
[db_host] => 127.0.0.1
)
db_port和cache键值都一致,被排除;db_host键相同但值不同,保留。这个用法比手写循环加if判断要简洁。
2.用户权限列表比对
权限系统里,用户权限常用["模块名"=>"权限级别"]的结构。判断某个用户的权限和角色默认权限有哪些差异,可以这样写:
<?php
$userPerm = [
"order" => "read",
"user" => "write",
"report" => "read"
];
$rolePerm = [
"order" => "read",
"user" => "read",
"report" => "read"
];
$extra = array_udiff_assoc($userPerm, $rolePerm, function ($a, $b) {
return strcmp($a, $b);
});
print_r($extra);
输出:
Array
(
[user] => write
)
这里回调用了strcmp(),它对字符串比较返回三态值,符合比较函数的要求。用strcmp()的好处是代码短,而且大小写敏感的行为明确。如果业务需要大小写不敏感,换成strcasecmp()即可。
3.回调返回值的坑
前面提到,array_udiff_assoc()只判断回调是否返回0。但有一个细节:PHP会把回调返回值转换为整数。如果返回null、false这类值,会被当成0,从而误判为“值相等”。所以回调里不要写return$a==$b;这种布尔返回,虽然false转成0看起来“碰巧能用”,但语义是错的,容易在维护时误导别人。明确写return$a==$b?0:1;更稳妥。
4.大数组的性能考虑
array_udiff_assoc()内部需要对多个数组做比较,复杂度不低。如果数组规模到几万条以上,且回调里还有复杂逻辑,性能会明显下降。我遇到过一次接口响应变慢,之后定位到是对一个上万条的关联数组做差集,回调里还做了字符串处理。优化方式是把比较逻辑前置,先对数组做一次键值规范化,再用简单回调比较,或者改用哈希表方式自己实现差集。函数本身没问题,问题在于用在了不合适的规模上。
5.键名类型要留意
PHP数组的键名有自动转换规则:数字字符串键名会被转成整数。比如"1"作为键名,实际存储时是整数1。如果两个数组一个用"1"、一个用1作键,它们在键名比较时会被视为同一个键。这个行为在大多数情况下不影响结果,但如果业务上确实需要区分字符串键和整数键,就要提前把键名处理成统一格式,避免出现意外匹配。
六、本节课程知识要点
-
array_udiff_assoc()求差集,方向是从第一个数组出发。 -
键名用内置规则比较,值用用户自定义回调比较。
-
回调函数是必填参数,返回值中
0表示值相等,非0表示不等。 -
返回值保留原数组的键名。
-
键名不一致的元素不会进入值比较环节。
-
与
array_udiff()、array_diff_assoc()、array_udiff_uassoc()的区别在于键名和值的比较方式组合不同。 -
回调返回布尔值会被转换为整数,建议明确返回
-1、0、1三态。 -
大数组场景要评估性能,必要时改用哈希表方式自行实现。
-
PHP数组键名有自动类型转换,数字字符串键会变成整数键。
七、常见问题
问:array_udiff_assoc和array_diff_assoc有什么区别?
array_diff_assoc()的键名和值都用PHP内置规则比较;array_udiff_assoc()的键名用内置规则、值用用户自定义回调。需要自定义值比较逻辑时用后者。
问:回调函数可以只返回0和1吗?
可以。差集判断只关心是否返回0,返回1和返回-1在结果上没有区别。但为了代码可读性和复用性,建议返回完整三态。
问:这个函数会修改原数组吗?
不会。它返回一个新数组,原数组保持不变。
问:多个数组比较时,顺序有影响吗?
有。差集的方向是“第一个数组中有、其他数组中没有”。第一个数组的位置不能随意调换,否则结果变。
问:键名相同但值类型不同,会被视为相等吗?
取决于回调怎么写。用==比较时,"1"和1会被视为相等;用===则不会。需要根据业务需求选择。
array_udiff_assoc()的核心是“键名按默认规则、值按自定义规则”求差集。用它的前提是数据结构本身是关联数组,且业务上确实需要同时考虑键名和值。如果只需要按值比较,用array_udiff()更合适;如果键名和值都要自定义,则应该看array_udiff_uassoc()。项目里常见的配置比对、权限比对场景,用这个函数能省掉一层手写循环。真正需要注意的是回调返回值规范、数组规模带来的性能问题,以及PHP键名自动转换这个底层行为。把这几点处理好,函数用起来是可靠的。