項目中有個功能是比較會員是否過期,review同事的代碼,發(fā)現(xiàn)其寫法比較奇葩,但線上竟也未出現(xiàn)bug。
實現(xiàn)大致如下:
$expireTime = "2014-05-01 00:00:00";$currentTime = date('Y-m-d H:i:s', time());if($currentTime < $expireTime) { return false;} else { return true;}如果兩個時間需要進(jìn)行比較,通常是轉(zhuǎn)換成unix時間戳,用兩個int型的數(shù)字進(jìn)行比較。該實現(xiàn)卻特意將時間表示成string,然后對兩個string進(jìn)行比較運(yùn)算。
撇開寫法不談,我很好奇的是php內(nèi)部是如何進(jìn)行比較的。
閑話少說,還是從源碼開始跟蹤。
編譯期在zend_language_parse.y中可以發(fā)現(xiàn)類似下述語法:
exPR === expr { zend_do_binary_op(ZEND_IS_IDENTICAL, &$$, &$1, &$3 TSRMLS_CC); }expr !== expr { zend_do_binary_op(ZEND_IS_NOT_IDENTICAL, &$$, &$1, &$3 TSRMLS_CC); }expr == expr { zend_do_binary_op(ZEND_IS_EQUAL, &$$, &$1, &$3 TSRMLS_CC); }expr != expr { zend_do_binary_op(ZEND_IS_NOT_EQUAL, &$$, &$1, &$3 TSRMLS_CC); }expr < expr { zend_do_binary_op(ZEND_IS_SMALLER, &$$, &$1, &$3 TSRMLS_CC); }expr <= expr { zend_do_binary_op(ZEND_IS_SMALLER_OR_EQUAL, &$$, &$1, &$3 TSRMLS_CC); }expr > expr { zend_do_binary_op(ZEND_IS_SMALLER, &$$, &$3, &$1 TSRMLS_CC); }expr >= expr { zend_do_binary_op(ZEND_IS_SMALLER_OR_EQUAL, &$$, &$3, &$1 TSRMLS_CC); }
很明顯,此處編譯成opcode用的便是zend_do_binary_op。
void zend_do_binary_op(zend_uchar op, znode *result, const znode *op1, const znode *op2 TSRMLS_DC) /* {{{ */{zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);opline->opcode = op;opline->result.op_type = IS_TMP_VAR;opline->result.u.var = get_temporary_variable(CG(active_op_array));opline->op1 = *op1;opline->op2 = *op2;*result = opline->result;}該函數(shù)并沒有做什么特別的處理,僅僅是簡單保存了opcode、操作數(shù)1和操作數(shù)2。
執(zhí)行期根據(jù)opcode,跳轉(zhuǎn)到相應(yīng)的處理函數(shù):ZEND_IS_SMALLER_SPEC_CONST_CONST_HANDLER。
static int ZEND_FASTCALL ZEND_IS_SMALLER_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS){zend_op *opline = EX(opline);zval *result = &EX_T(opline->result.u.var).tmp_var;compare_function(result,&opline->op1.u.constant,&opline->op2.u.constant TSRMLS_CC);ZVAL_BOOL(result, (Z_LVAL_P(result) < 0));ZEND_VM_NEXT_OPCODE();}注意到,兩個zval的比較是利用compare_function來處理。
ZEND_API int compare_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ */{int ret;int converted = 0;zval op1_copy, op2_copy;zval *op_free;while (1) {switch (TYPE_PAIR(Z_TYPE_P(op1), Z_TYPE_P(op2))) {case TYPE_PAIR(IS_LONG, IS_LONG):...case TYPE_PAIR(IS_DOUBLE, IS_LONG):...case TYPE_PAIR(IS_DOUBLE, IS_DOUBLE):......// 兩個字符串進(jìn)行比較case TYPE_PAIR(IS_STRING, IS_STRING):zendi_smart_strcmp(result, op1, op2);return SUCCESS;...}}}該函數(shù)例舉了若干種情況,根據(jù)本文case,進(jìn)入zendi_smart_strcmp一窺究竟:
ZEND_API void zendi_smart_strcmp(zval *result, zval *s1, zval *s2) /* {{{ */{int ret1, ret2;long lval1, lval2;double dval1, dval2;// 嘗試將字符串轉(zhuǎn)成數(shù)字類型if ((ret1=is_numeric_string(Z_STRVAL_P(s1), Z_STRLEN_P(s1), &lval1, &dval1, 0)) &&(ret2=is_numeric_string(Z_STRVAL_P(s2), Z_STRLEN_P(s2), &lval2, &dval2, 0))) {// 進(jìn)行數(shù)字之間的比較...} else { // 無法全部轉(zhuǎn)成數(shù)字 // 則調(diào)用zend_binary_zval_strcmp // 本質(zhì)為memcmp的一層封裝Z_LVAL_P(result) = zend_binary_zval_strcmp(s1, s2);ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_LVAL_P(result)));}}那么“2014-05-01 00:00:00”能否轉(zhuǎn)化成數(shù)字么?
還是得看下is_numeric_string的實現(xiàn)規(guī)則。
static inline zend_uchar is_numeric_string(const char *str, int length, long *lval, double *dval, int allow_errors){const char *ptr;int base = 10, digits = 0, dp_or_e = 0;double local_dval;zend_uchar type;if (!length) {return 0;}/* trim掉字符串開頭的空白部分 */while (*str == ' ' || *str == '/t' || *str == '/n' || *str == '/r' || *str == '/v' || *str == '/f') {str++;length--;}ptr = str;if (*ptr == '-' || *ptr == '+') {ptr++;}if (ZEND_IS_DIGIT(*ptr)) {/* 判斷是否為16進(jìn)制*/if (length > 2 && *str == '0' && (str[1] == 'x' || str[1] == 'X')) {base = 16;ptr += 2;}/* 忽略后續(xù)的若干0 */while (*ptr == '0') {ptr++;}/* 計算數(shù)字的位數(shù),并決定是整型還是浮點(diǎn) */for (type = IS_LONG; !(digits >= MAX_LENGTH_OF_LONG && (dval || allow_errors == 1)); digits++, ptr++) {check_digits:if (ZEND_IS_DIGIT(*ptr) || (base == 16 && ZEND_IS_XDIGIT(*ptr))) {continue;} else if (base == 10) {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 (base == 10) {if (digits >= MAX_LENGTH_OF_LONG) {dp_or_e = -1;goto process_double;}} else if (!(digits < SIZEOF_LONG * 2 || (digits == SIZEOF_LONG * 2 && ptr[-digits] <= '7'))) {if (dval) {local_dval = zend_hex_strtod(str, (char **)&ptr);}type = IS_DOUBLE;}} else if (*ptr == '.' && ZEND_IS_DIGIT(ptr[1])) {// 處理浮點(diǎn)數(shù)} 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");}}// 允許容錯,則嘗試將str轉(zhuǎn)成數(shù)字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);}return IS_DOUBLE;}}if (lval) {*lval = strtol(str, NULL, base);}return IS_LONG;} else {if (dval) {*dval = local_dval;}return IS_DOUBLE;}}代碼比較長,不過仔細(xì)閱讀,str轉(zhuǎn)num的規(guī)則還是很清晰的。
尤其注意的是allow_errors這個參數(shù),它直接決定了本例中無法將“2014-05-01 00:00:00”轉(zhuǎn)化成數(shù)字。
因而最后其實“2014-04-17 00:00:00” < “2014-05-01 00:00:00” 的運(yùn)行是走的memcmp分支。
既然是memcmp,便不難理解為何文章開始提到的寫法也能正確運(yùn)行。
容錯轉(zhuǎn)換何時allow_errors為true呢?一個極好的例子便是zend_parse_parameters,zend_parse_parameters的實現(xiàn)不再細(xì)述,有興趣的讀者可以自行研究。其中調(diào)用is_numeric_string時將allow_errors置為了-1。
舉個例子:
static void php_date(INTERNAL_FUNCTION_PARAMETERS, int localtime){char *format;int format_len;long ts;char *string;// 期望的第二個參數(shù)為timestamp,為long// 假設(shè)上層調(diào)用時,誤傳入了string,那么zend_parse_parameters依然會盡可能的嘗試將string解析為longif (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l", &format, &format_len, &ts) == FAILURE) {RETURN_FALSE;}if (ZEND_NUM_ARGS() == 1) {ts = time(NULL);}string = php_format_date(format, format_len, ts, localtime TSRMLS_CC);RETVAL_STRING(string, 0);}這是php的date函數(shù)內(nèi)部實現(xiàn)。
在我們調(diào)用date時,如果將第二個參數(shù)傳入string,效果如下:
echo date('Y-m-d', '0-1-2');// 輸出PHP Notice: A non well formed numeric value encountered in Command line code on line 11970-01-01雖然報出notice級別的錯誤,但依然成功將'0-1-2'轉(zhuǎn)成了0
新聞熱點(diǎn)
疑難解答