array_diff_uassoc 那些不为人知的一面

如果让你用一句话描述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]

你了解的真的对吗?

自定义函数比较的是两个数组的键吗?

其实,说实话,一开始我也是这么认为的。直到我在自定义函数中分别输出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

所以可以看出来,传入自定义函数进行比较的不一定是来自不同数组的键。还有可能是相同数组的键。

自定义函数只是比较键值是否相等吗?

当然不是了,这个比较函数本身是比较大小的。但是却不是我们理解的比较键值是否相等的。根据自定的返回结果,php内部会对内部的指针位置进行调整,所以我们看到后面的比较是a-e b-e c-e d-e

比较键值的时候,真的是相同健名的数组元素键值相比较吗?

这个也不是的。实际上就是因为比较函数的数组结果回影响到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内部首先对所有的输入数组进行进行排序。所以在自定义函数中可以看出前面的输出内容都是先把数组的键名依次进行比较。

真实面目

当输入的数组的都按键名拍好序之后,就要对第一个数组分别于其他数组的键名进行比较。

比较第一个数组当前元素的键名与要比较数组的各个元素健名是否一样,知道遇到第一个一样或者比较结束为止。
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++;
      }
    }
    ...
  }
  ...
}
如果键名一样(健名比较函数返回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;
                }
            }
            ...
        }
        ...
    }
    ...
}
根据比较结果,如果比较结果不相等,则用第一个数组的下一个元素比较其他数组的所有元素。

如果比较结果相等(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的老王】2019.10.10
PS:发文不易,如果您觉得文章对您有帮助,帮忙点赞哟。

本作品采用《CC 协议》,转载必须注明作者和本文链接
写PHP的老王
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 1

第一句的函数应该是数组吧.

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!