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"}
這才是我們想要的結果。