共计 3677 个字符,预计需要花费 10 分钟才能阅读完成。
如果让你用一句话描述 PHP 函数array_diff_uassoc
,也许你开口就来了,就是同时比较两个或多个函数,并返回在第一个函数出现且没有在其他函数出现的键值同时相同的数据。
最近看到一个很有意思的问题,问的是关于 array_diff_uassoc
执行阅读这个问题才明白对这个函数的误解有多深。
下面是问题的简化版本:
function comparekey($a,$b){return 0;}
$array1 = ['a'=>1,'b'=>2,'c'=>3,'d'=>4];
$array2 = ['a'=>2,'d'=>4,'e'=>6];
$res = array_diff_uassoc($array1,$array2,'comparekey');
var_dump($res);
为什么结果是
['a'=>1,'c'=>3,'d'=>4];
按正常逻辑,array_diff_uassoc
返回 key 不一样,且值不一样的数组数据。自定义比较函数返回 0 则认为 key 值一样。所以正常逻辑应该返回的是
['a'=>1,'b'=>2,'c'=>3]
你了解的真的对吗?
1. 自定义函数比较的是两个数组的键吗?
其实,说实话,一开始我也是这么认为的。直到我在自定义函数中分别输出 a,b,看到那奇葩的输出内容才觉得,那个比较函数没那么简单。
为了方便看出内容,使用下面的数组替代问题中的数组内容
function comparekey($a,$b){
echo $a.'-'.$b;
return 0;
}
$array1 = ['a'=>1,'b'=>2,'c'=>3,'d'=>4];
$array2 = ['e'=>'2','f'=>5,'g'=>6];
$res = array_diff_uassoc($array1,$array2,'comparekey');
函数输出内容为
a-b b-c c-d e-f f-g a-e b-e c-e d-e
所以可以看出来,传入自定义函数进行比较的 不一定 是来自不同数组的键。还有可能是相同数组的键。
2. 自定义函数只是比较键值是否相等吗?
当然不是了,这个比较函数本身是比较大小的。但是却不是我们理解的比较键值是否相等的。根据自定的返回结果,php 内部会对内部的指针位置进行调整,所以我们看到后面的比较是 a -e b-e c-e d-e
3. 比较键值的时候,真的是相同健名的数组元素键值相比较吗?
这个也不是的。实际上就是因为比较函数的数组结果回影响到 php 内部数组指针位置的变更。变更方式不同会导致最终相互比价的不是我们认为的相同键名的值相互比较。
看一下 php 源码,array_diff_uassoc 最终都是通过 php_array_diff
函数实现的。
static void php_array_diff(void *base, size_t nmemb, size_t siz, compare_func_t cmp, swap_func_t swp)
{
...
if (hash->nNumOfElements > 1) {if (behavior == DIFF_NORMAL) {zend_sort((void *) lists[i], hash->nNumOfElements,
sizeof(Bucket), diff_data_compare_func, (swap_func_t)zend_hash_bucket_swap);
} else if (behavior & DIFF_ASSOC) { /* triggered also when DIFF_KEY */
zend_sort((void *) lists[i], hash->nNumOfElements,
sizeof(Bucket), diff_key_compare_func, (swap_func_t)zend_hash_bucket_swap);
}
}
...
}
可以看到 diff_key_compare_func
传给了排序函数。所以,自定义函数的返回结果会影响到临时变量 lists
的输出。
php 内部首先对所有的输入数组进行进行排序。所以在自定义函数中可以看出前面的输出内容都是先把数组的键名依次进行比较。
真实面目
当输入的数组的都按键名拍好序之后,就要对第一个数组分别于其他数组的键名进行比较。
1) 比较第一个数组当前元素的键名与要比较数组的各个元素健名是否一样,知道遇到第一个一样或者比较结束为止。
RETVAL_ARR(zend_array_dup(Z_ARRVAL(args[0])));
while (Z_TYPE(ptrs[0]->val) != IS_UNDEF) {for (i = 1; i < arr_argc; i++) {Bucket *ptr = ptrs[i];
if (behavior == DIFF_NORMAL) {while (Z_TYPE(ptrs[i]->val) != IS_UNDEF && (0 < (c = diff_data_compare_func(ptrs[0], ptrs[i])))) {ptrs[i]++;
}
} else if (behavior & DIFF_ASSOC) { /* triggered also when DIFF_KEY */
while (Z_TYPE(ptr->val) != IS_UNDEF && (0 != (c = diff_key_compare_func(ptrs[0], ptr)))) {ptr++;}
}
...
}
...
}
2) 如果键名一样(健名比较函数返回 0),则比较键值是否相等。如果不相等,则 c 设置为 -1,继续比较下一个数组的元素。
RETVAL_ARR(zend_array_dup(Z_ARRVAL(args[0])));
while (Z_TYPE(ptrs[0]->val) != IS_UNDEF) {
...
for (i = 1; i < arr_argc; i++) {
...
if (!c) {
...
if (diff_data_compare_func(ptrs[0], ptr) != 0) {
c = -1;
if (key_compare_type == DIFF_COMP_KEY_USER) {BG(user_compare_fci) = *fci_key;
BG(user_compare_fci_cache) = *fci_key_cache;
}
}
...
}
...
}
...
}
3) 根据比较结果,如果比较结果不相等,则用第一个数组的下一个元素比较其他数组的所有元素。
如果比较结果相等 (c=0), 则删除返回数组(第一个数组复制得到的) 对应的键名。
RETVAL_ARR(zend_array_dup(Z_ARRVAL(args[0])));
while (Z_TYPE(ptrs[0]->val) != IS_UNDEF) {
...
if (!c) {for (;;) {p = ptrs[0];
p = ptrs[0];
if (p->key == NULL) {zend_hash_index_del(Z_ARRVAL_P(return_value), p->h);
} else {zend_hash_del(Z_ARRVAL_P(return_value), p->key);
}
if (Z_TYPE((++ptrs[0])->val) == IS_UNDEF) {goto out;}
...
}
}
else {for (;;) {if (Z_TYPE((++ptrs[0])->val) == IS_UNDEF) {goto out;}
...
}
...
}
...
}
以下列数组以及自定义函数为例说明比较过程。
function comparekey($a,$b){return 0;}
$array1 = ['a'=>1,'b'=>2,'c'=>3,'d'=>4];
$array2 = ['a'=>2,'d'=>4,'e'=>6];
设置返回数组未 array1
比较健名 ”a”,”a” 相等,则比较 array1[‘a’]!=$array2[‘a’]。
比较健名 ”b”,”a”, 相等,则比较 array1[‘b’]==$array2[‘a’], 删除返回数组的键值 ’b’
比较健名 ”c”,”a”, 相等,则比较 array1[‘c’]!=$array2[‘a’]。
比较健名 ”d”,”a”, 相等,则比较 array1[‘c’]!=$array2[‘a’]。
所以最终返回数组为
$res = ['a'=>1,'c'=>3,'d'=>4]
总结
所以,自定义函数并不是让我们完全的自定义。自定义的函数返回结果回导致不一样的输出结果。php 数组有很多提供自定义的函数方法。但是,如果你的自定义函数返回值是“有悖常理的”,比如这个问题中的函数,永远都是相等的,但是 php 同一个数组的键值不可能相同,所以这个自定义函数的比较结果其实是 ” 有问题的 ”。在这个前提下,那么 php 返回的结果也有可能会有意外的输出。
当你下次使用 array_diff_uassoc
函数的时候,应该了解到,这个自定义函数并不仅仅是比较两个数组的健名是否一样,还会影响到比较之前 php 对输入数组的内部排序;自定义函数的返回结果会直接影响到 php 数组指针的变更顺序,导致比较结果的不一样;
文章首发于公众【写 PHP 的老王】
PS: 分享不易,如果觉得有用,记得分享哟