array_diff_uassoc 那些不為人知的一面

寫PHP的老王發表於2019-10-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]

你瞭解的真的對嗎?

自定義函式比較的是兩個陣列的鍵嗎?

其實,說實話,一開始我也是這麼認為的。直到我在自定義函式中分別輸出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的老王

相關文章