php 中 SORT_REGULAR 和 SORT_STRING 的區別

來源:互聯網
上載者:User

asort 的問題

有一次在使用 php 基本函數 asort 的時候遇到了一個問題:

 "441469",        "timestamp" => "1464334314"    ];    asort($arr);    var_dump($arr);?>

這樣排序出來的結果是:

array(2) {    ["nonce_str"]=>    string(6) "441469"    ["timestamp"]=>    string(10) "1464334314"}

WTF?

不應該呀,為什麼排序出來 4 在 1 的前面呢,字串不應該是以字串比較的方式來排序嗎?

查閱 php.net 參考文檔得知 sort 類函數的第二個參數為 SORT_FLAG:

SORT_REGULAR - 正常比較單元(不改變類型)

SORT_NUMERIC - 單元被作為數字來比較

SORT_STRING - 單元被作為字串來比較

SORT_LOCALE_STRING - 根據當前的地區(locale)設定來把單元當作字串比較,可以用 setlocale() 來改變。

SORT_NATURAL - 和 natsort() 類似對每個單元以“自然的順序”對字串進行排序。 PHP 5.4.0 中新增的。

SORT_FLAG_CASE - 能夠與 SORT_STRING 或 SORT_NATURAL 合并(OR 位元運算),不區分大小寫排序字串。

看來預設的排序方式是所謂的 SORT_REGULAR。可是,“正常比較單元”是什麼意思呢?“不改變類型”又是指什麼,既然不改變類型,那麼我的兩個字串就應該以字串比較 strcmp 的順序排列吧,也就是說,字串 "1464334314" 應該在 "441469" 前面才對!帶著這些疑問,我找到了 php 的原始碼。

源碼分析

http://git.php.net/?p=php-src.git;a=blob_plain;f=ext/standard/array.c;hb=42be298b3020337653cfcbdd87698b90006b2197

php-src.git/ext/standard/array.c, 887:

/* {{{ proto bool asort(array &array_arg [, int sort_flags])   Sort an array and maintain index association */PHP_FUNCTION(asort){    zval *array;    zend_long sort_type = PHP_SORT_REGULAR;    compare_func_t cmp;    if (zend_parse_parameters(ZEND_NUM_ARGS(), "a/|l", &array, &sort_type) == FAILURE) {        RETURN_FALSE;    }    cmp = php_get_data_compare_func(sort_type, 0);    if (zend_hash_sort(Z_ARRVAL_P(array), cmp, 0) == FAILURE) {        RETURN_FALSE;    }    RETURN_TRUE;}/* }}} */

可以看到,進行具體比較順序控制的函數指標是 cmp,是通過向 php_get_data_compare_func 傳入 sort_type 得到的,sort_type 也就是 SORT_REGULAR / SORT_STRING 這樣的標記。

php-src.git/ext/standard/array.c, 632:

static compare_func_t php_get_data_compare_func(zend_long sort_type, int reverse) /* {{{ */{    switch (sort_type & ~PHP_SORT_FLAG_CASE) {        // ...        case PHP_SORT_STRING:            if (sort_type & PHP_SORT_FLAG_CASE) {                if (reverse) {                    return php_array_reverse_data_compare_string_case;                } else {                    return php_array_data_compare_string_case;                }            } else {                if (reverse) {                    return php_array_reverse_data_compare_string;                } else {                    return php_array_data_compare_string;                }            }            break;        // ...        case PHP_SORT_REGULAR:        default:            if (reverse) {                return php_array_reverse_data_compare;            } else {                return php_array_data_compare;            }            break;    }    return NULL;}/* }}} */

在這個函數中我們可以看到,SORT_REGULAR 採用了 php_array_data_compare 進行比較,而 SORT_STRING 採用了 php_array_data_compare_string 進行比較。

php-src.git/ext/standard/array.c, 370:

/* Numbers are always smaller than strings int this function as it * anyway doesn't make much sense to compare two different data types. * This keeps it consistent and simple. * * This is not correct any more, depends on what compare_func is set to. */static int php_array_data_compare(const void *a, const void *b) /* {{{ */{    // ...    if (compare_function(&result, first, second) == FAILURE) {        return 0;    }    ZEND_ASSERT(Z_TYPE(result) == IS_LONG);    return Z_LVAL(result);}/* }}} */

php-src.git/ext/standard/array.c, 465:

static int php_array_data_compare_string(const void *a, const void *b) /* {{{ */{    // ...    return string_compare_function(first, second);}/* }}} */

現在我們得到了兩條調用鏈:

SORT_REGULAR -> php_get_data_compare_func -> php_array_data_compare -> compare_function;

SORT_STRING -> php_get_data_compare_func -> php_array_data_compare_string -> string_compare_function;

SORT_REGULAR

先從第一條的 compare_function 開始:

http://git.php.net/?p=php-src.git;a=blob_plain;f=Zend/zend_operators.c;hb=42be298b3020337653cfcbdd87698b90006b2197

php-src.git/Zend/zend_operators.c, 1818:

ZEND_API int ZEND_FASTCALL compare_function(zval *result, zval *op1, zval *op2) /* {{{ */{    // ...    while (1) {        switch (TYPE_PAIR(Z_TYPE_P(op1), Z_TYPE_P(op2))) {            // ...            case TYPE_PAIR(IS_STRING, IS_STRING):                if (Z_STR_P(op1) == Z_STR_P(op2)) {                    ZVAL_LONG(result, 0);                    return SUCCESS;                }                ZVAL_LONG(result, zendi_smart_strcmp(Z_STR_P(op1), Z_STR_P(op2)));                return SUCCESS;            // ...        }    }}/* }}} */

SORT_REGULAR -> php_get_data_compare_func -> php_array_data_compare -> compare_function -> zendi_smart_strcmp;

php-src.git/Zend/zend_operators.c, 2681:

ZEND_API zend_long ZEND_FASTCALL zendi_smart_strcmp(zend_string *s1, zend_string *s2) /* {{{ */{    int ret1, ret2;    int oflow1, oflow2;    zend_long lval1 = 0, lval2 = 0;    double dval1 = 0.0, dval2 = 0.0;    if ((ret1 = is_numeric_string_ex(s1->val, s1->len, &lval1, &dval1, 0, &oflow1)) &&        (ret2 = is_numeric_string_ex(s2->val, s2->len, &lval2, &dval2, 0, &oflow2))) {#if ZEND_ULONG_MAX == 0xFFFFFFFF        if (oflow1 != 0 && oflow1 == oflow2 && dval1 - dval2 == 0. &&            ((oflow1 == 1 && dval1 > 9007199254740991. /*0x1FFFFFFFFFFFFF*/)            || (oflow1 == -1 && dval1 < -9007199254740991.))) {#else        if (oflow1 != 0 && oflow1 == oflow2 && dval1 - dval2 == 0.) {#endif            /* both values are integers overflown to the same side, and the             * double comparison may have resulted in crucial accuracy lost */            goto string_cmp;        }        if ((ret1 == IS_DOUBLE) || (ret2 == IS_DOUBLE)) {            if (ret1 != IS_DOUBLE) {                if (oflow2) {                    /* 2nd operand is integer > LONG_MAX (oflow2==1) or < LONG_MIN (-1) */                    return -1 * oflow2;                }                dval1 = (double) lval1;            } else if (ret2 != IS_DOUBLE) {                if (oflow1) {                    return oflow1;                }                dval2 = (double) lval2;            } else if (dval1 == dval2 && !zend_finite(dval1)) {                /* Both values overflowed and have the same sign,                 * so a numeric comparison would be inaccurate */                goto string_cmp;            }            dval1 = dval1 - dval2;            return ZEND_NORMALIZE_BOOL(dval1);        } else { /* they both have to be long's */            return lval1 > lval2 ? 1 : (lval1 < lval2 ? -1 : 0);        }    } else {        int strcmp_ret;string_cmp:        strcmp_ret = zend_binary_strcmp(s1->val, s1->len, s2->val, s2->len);        return ZEND_NORMALIZE_BOOL(strcmp_ret);    }}/* }}} */

關鍵來了,這裡我們可以看到,zendi_smart_strcmp 先判斷需要比較的兩個字串是否是以數位形式表現的類型。如果是,則將“數字”一樣的字串作為整型或浮點型進行比較,如果不是,則將字串用 zend_binary_strcmp 進行比較。

php-src.git/Zend/zend_operators.c, 2791:

ZEND_API zend_uchar ZEND_FASTCALL _is_numeric_string_ex(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors, int *oflow_info) /* {{{ */{    const char *ptr;    int digits = 0, dp_or_e = 0;    double local_dval = 0.0;    zend_uchar type;    zend_long tmp_lval = 0;    int neg = 0;    if (!length) {        return 0;    }    if (oflow_info != NULL) {        *oflow_info = 0;    }    /* Skip any whitespace     * This is much faster than the isspace() function */    while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r' || *str == '\v' || *str == '\f') {        str++;        length--;    }    ptr = str;    if (*ptr == '-') {        neg = 1;        ptr++;    } else if (*ptr == '+') {        ptr++;    }    if (ZEND_IS_DIGIT(*ptr)) {        /* Skip any leading 0s */        while (*ptr == '0') {            ptr++;        }        /* Count the number of digits. If a decimal point/exponent is found,         * it's a double. Otherwise, if there's a dval or no need to check for         * a full match, stop when there are too many digits for a long */        for (type = IS_LONG; !(digits >= MAX_LENGTH_OF_LONG && (dval || allow_errors == 1)); digits++, ptr++) {check_digits:            if (ZEND_IS_DIGIT(*ptr)) {                tmp_lval = tmp_lval * 10 + (*ptr) - '0';                continue;            } else if (*ptr == '.' && dp_or_e < 1) {                goto process_double;            } else if ((*ptr == 'e' || *ptr == 'E') && dp_or_e < 2) {                const char *e = ptr + 1;                if (*e == '-' || *e == '+') {                    ptr = e++;                }                if (ZEND_IS_DIGIT(*e)) {                    goto process_double;                }            }            break;        }        if (digits >= MAX_LENGTH_OF_LONG) {            if (oflow_info != NULL) {                *oflow_info = *str == '-' ? -1 : 1;            }            dp_or_e = -1;            goto process_double;        }    } else if (*ptr == '.' && ZEND_IS_DIGIT(ptr[1])) {process_double:        type = IS_DOUBLE;        /* If there's a dval, do the conversion; else continue checking         * the digits if we need to check for a full match */        if (dval) {            local_dval = zend_strtod(str, &ptr);        } else if (allow_errors != 1 && dp_or_e != -1) {            dp_or_e = (*ptr++ == '.') ? 1 : 2;            goto check_digits;        }    } else {        return 0;    }    if (ptr != str + length) {        if (!allow_errors) {            return 0;        }        if (allow_errors == -1) {            zend_error(E_NOTICE, "A non well formed numeric value encountered");        }    }    if (type == IS_LONG) {        if (digits == MAX_LENGTH_OF_LONG - 1) {            int cmp = strcmp(&ptr[-digits], long_min_digits);            if (!(cmp < 0 || (cmp == 0 && *str == '-'))) {                if (dval) {                    *dval = zend_strtod(str, NULL);                }                if (oflow_info != NULL) {                    *oflow_info = *str == '-' ? -1 : 1;                }                return IS_DOUBLE;            }        }        if (lval) {            if (neg) {                tmp_lval = -tmp_lval;            }            *lval = tmp_lval;        }        return IS_LONG;    } else {        if (dval) {            *dval = local_dval;        }        return IS_DOUBLE;    }}/* }}} */

上面的代碼是判斷字串是否為“數字”形式的比較函數。

SORT_STRING

然後我們看看第二條調用鏈:SORT_STRING -> php_get_data_compare_func -> php_array_data_compare_string -> string_compare_function;

php-src.git/Zend/zend_operators.c, 1729:

ZEND_API int ZEND_FASTCALL string_compare_function(zval *op1, zval *op2) /* {{{ */{    if (EXPECTED(Z_TYPE_P(op1) == IS_STRING) &&        EXPECTED(Z_TYPE_P(op2) == IS_STRING)) {        if (Z_STR_P(op1) == Z_STR_P(op2)) {            return 0;        } else {            return zend_binary_strcmp(Z_STRVAL_P(op1), Z_STRLEN_P(op1), Z_STRVAL_P(op2), Z_STRLEN_P(op2));        }    } else {        zend_string *str1 = zval_get_string(op1);        zend_string *str2 = zval_get_string(op2);        int ret = zend_binary_strcmp(ZSTR_VAL(str1), ZSTR_LEN(str1), ZSTR_VAL(str2), ZSTR_LEN(str2));        zend_string_release(str1);        zend_string_release(str2);        return ret;    }}/* }}} */

可以看到,SORT_STRING 最終也使用 zend_binary_strcmp 函數進行字串比較。下面的代碼,是 zend_binary_strcmp 的實現:

php-src.git/Zend/zend_operators.c, 2539:

ZEND_API int ZEND_FASTCALL zend_binary_strcmp(const char *s1, size_t len1, const char *s2, size_t len2) /* {{{ */{    int retval;    if (s1 == s2) {        return 0;    }    retval = memcmp(s1, s2, MIN(len1, len2));    if (!retval) {        return (int)(len1 - len2);    } else {        return retval;    }}/* }}} */

總結

經過以上分析我們可以得知,SORT_STRING 排序方式的底層實現是 C 語言的 memcmp,也就是說,它對兩個字串從前往後,按照逐個位元組比較,一旦位元組有差異,就終止並比較出大小。

而 SORT_REGULAR 會智能判斷需排序對象的類型,如果兩個字串都是“純數字”形式的字串,會以比較整個字串所代表的十進位整數、浮點數大小的形式進行排序。如果兩個字串不是“純數字“形式的,才會和 SORT_STRING 一樣。

因此,如果需要以字串 strcmp 方式逐個位元組從前往後比較來進行排序,在調用 php 的 sort 類函數的時候請務必使用 SORT_STRING 這個 flag,否則如果兩個字串都是”純數字“形式的,就會按照它們所代表的數字大小進行排序。

而且需要注意的是,如果兩個值的類型不同,那麼這樣的比較是毫無意義的,也可能會產生意想不到的結果。

最後的測試

最後,我們在欲排序的值最後添加了一個字元 "s",使它們不再是”純數字“形式的字串:

 "441469s",        "timestamp" => "1464334314s"    ];    asort($arr);    var_dump($arr);?>

最後排序的結果變成了:

array(2) {    ["timestamp"]=>    string(11) "1464334314s"    ["nonce_str"]=>    string(7) "441469s"}

這才是我們想要的結果。

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.