Правильное использование array_udiff

Вы хорошо знаете php? Тогда вы безусловно знаете, что такое array_udiff. Эта функция делает сравнение нескольких массивов и возвращает массив, элементы, которык есть в первом переданном массиве, но нет в остальных. Для сравнения используется пользовательская функция. Все верно, ничего не забыл?

А сможете сказать, сколько будет i в конце и почему именно так:

<?php
$ar1 = [1, 2];
$ar2 = [4, 5];
$i = 0;
$ar1 = array_udiff($ar1, $ar2, function ($a, $b) use ($ar1, $ar2, &$i){
    echo 'i ' . ($i++) . ' a ' . $a. ' b '. $b. "\n";
    if (($a == $b)) {
        return 0;
    }
    return 1;
});
var_dump($ar1, $i);
?>

Должно получиться 5. Еще более интересным выглядит дамп переменных в пользовательской функции:

i 0 a 2 b 1
i 1 a 5 b 4
i 2 a 1 b 4
i 3 a 1 b 5
i 4 a 1 b 2

Давайте разбираться, почему так.

Сегодня я писал код, который сравнивает массивы, состоящие из объектов, и определяет логическую разницу между ними. По документации функция сравнения должна быть:

Функция сравнения должна возвращать целое, которое меньше, равно или больше нуля, если первый аргумент является соответственно меньшим, равным или большим чем второй.

Мне же нужна простая разница (есть объект во втором массиве или его нет), поэтому я упросил функцию сравнения:

 function (Tariff $a, Tariff $b) {
    if (($a->type_id == $b->type_id)) {
        return 0;
    }
    return 1;
}

Результаты работы были не то что неверными, они были странными. Внешне все выглядит, как если бы функция была сломана. Попробовав сделать еще несколько модификаций кода, получил, что иногда код работает, а иногда нет. Однако, если попробовать примеры из документации, то все они работали как ожидается.

Устроив вместе с коллегами мозговой штурм, мы пришли к выводу, что дело в каком-то неожиданном для нас поведении функции. Самым простым вариантов определить что не так - это исследование исходного кода:

// https://github.com/php/php-src/blob/f2b6b261893f5b3448afb7cff6cf859a4d82a0de/ext/standard/array.c

    /* for each argument, create and sort list with pointers to the hash buckets */
    lists = (Bucket **)safe_emalloc(arr_argc, sizeof(Bucket *), 0);
    ptrs = (Bucket **)safe_emalloc(arr_argc, sizeof(Bucket *), 0);

    if (behavior == DIFF_NORMAL && data_compare_type == DIFF_COMP_DATA_USER) {
        BG(user_compare_fci) = *fci_data;
        BG(user_compare_fci_cache) = *fci_data_cache;
    } else if (behavior & DIFF_ASSOC && key_compare_type == DIFF_COMP_KEY_USER) {
        BG(user_compare_fci) = *fci_key;
        BG(user_compare_fci_cache) = *fci_key_cache;
    }

    for (i = 0; i < arr_argc; i++) {
        if (Z_TYPE(args[i]) != IS_ARRAY) {
            php_error_docref(NULL, E_WARNING, "Argument #%d is not an array", i + 1);
            arr_argc = i; /* only free up to i - 1 */
            goto out;
        }
        hash = Z_ARRVAL(args[i]);
        list = (Bucket *) pemalloc((hash->nNumOfElements + 1) * sizeof(Bucket), hash->u.flags & HASH_FLAG_PERSISTENT);
        if (!list) {
            PHP_ARRAY_CMP_FUNC_RESTORE();

            efree(ptrs);
            efree(lists);
            RETURN_FALSE;
        }
        lists[i] = list;
        ptrs[i] = list;
        for (idx = 0; idx < hash->nNumUsed; idx++) {
            p = hash->arData + idx;
            if (Z_TYPE(p->val) == IS_UNDEF) continue;
            *list++ = *p;
        }
        ZVAL_UNDEF(&list->val);
        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);
            }
        }
    }

Самым интерсным является первый комментарий и последний if.Для каждого переданного аргумента создается новый лист и делается сортировка.

В это момент все становится на свои места. Функция array_udiff сделана для быстрой нахождении логической разницы между массивами, для этого она вначале сортирует объекты в каждом из массивов отдельно, а потом делает разность слиянием. Когда я написал упрощенную функцию сравнения без значения -1, я фактически сломал сортировку массивов, и функция стала давать непредсказуемые результаты.

Для себя я из этой истории я вынес, что нужно не только знать, как нужно делать по документации, но еще и очень полезно знать как документацию правильно нарушать.

Posted in PHP on Mar 23, 2016

comments powered by Disqus