最近在為公司面試新人,經常會問到的一道題目就是PHP類型轉換的值,例如:
var_dump((int)true);
var_dump((string)true);
var_dump((string)false);
var_dump((bool)"1");
var_dump((bool)"0");
var_dump((bool)"");
var_dump((bool)"false");
我印象中最早見到這道題目是在英極的PHP進階開發工程師崗位的筆試題裡面,看似很基礎,但是依然可以難住不少PHPer。先來看一下運行結果:
int(1)
string(1) "1"
string(0) ""
bool(true)
bool(false)
bool(false)
bool(true)
對於大多數人來說,第1、2、4行通常是沒有問題的。但是為什麼false轉換為字串是Null 字元串呢?在處理請求值時,通常會傳一個字串類型的false,但是“false”(字串)並非false(布爾),這有點令人疑惑了。
為什麼會這樣呢?
關於這個問題,我們從PHP核心入手,看看在類型轉換時系統內部到底發生了什麼。
首先補充一些關於PHP弱類型實現方式的背景知識。PHP解譯器是使用C語言寫成的,當然最終對變數的處理,也會使用C語言構造資料結構來實現。在Zend引擎中,一個PHP變數對應的類型是zval。
開啟Zend/zend_types.h檔案,我們可以看到zval類型的定義,php-5.5.23版本大約在第55行左右:
typedef struct _zval_struct zval;
這樣我們發現,zval其實是一個名為_zval_struct的結構體類型,我們在Zend/zend.h檔案中找到這個結構體的定義,大約在320行左右開始:
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
大家可以看到,_zval_struct中包含兩個重要的成員,一個是zvalue_value類型的value,一個是zend_uchar類型的type。注意zvalue_value類型是一個聯合體,它用來儲存一個PHP變數的值的資訊。(如果你忘記了什麼是聯合體,我來解釋一下。聯合體類似結構體,但是聯合體的中的成員,儲存時有且只能有一個,而且聯合體佔用的空間是聯合體中長度最長的那個成員,這樣做是為了節省記憶體的使用。)在zvalue_value中,包括了long、double、struct、HashTable、zend_object_value五個類型的成員。他們分別用來儲存PHP變數不同類型的值:
C類型 PHP類型
long bool
int
resource
double float
struct string
HashTable array
zend_object_value object
看到這個結構體之後,想必也就明白了常問的諸如PHP中int類型的取值範圍,以及php中strlen的時間複雜度之類的問題。
由此可見,PHP的變數類型轉換,或者說是弱類型實現,本質上是實現zval類型在不同類型之間的轉換。除了完成zvalue_value的數值轉換,還需要將_zval_struct中的type設定成當前變數的type類型。在Zend引擎中實現了convert_to_*系列函數完成這一轉換,我們在Zend/zend_operators.c中可以看到這些轉換函式,在大約511行左右,可以找到轉換為布爾類型的函數:
ZEND_API void convert_to_boolean(zval *op) /* {{{ */
{
int tmp;
switch (Z_TYPE_P(op)) {
case IS_BOOL:
break;
case IS_NULL:
Z_LVAL_P(op) = 0;
break;
case IS_RESOURCE: {
TSRMLS_FETCH();
zend_list_delete(Z_LVAL_P(op));
}
/* break missing intentionally */
case IS_LONG:
Z_LVAL_P(op) = (Z_LVAL_P(op) ? 1 : 0);
break;
case IS_DOUBLE:
Z_LVAL_P(op) = (Z_DVAL_P(op) ? 1 : 0);
break;
case IS_STRING:
{
char *strval = Z_STRVAL_P(op);
if (Z_STRLEN_P(op) == 0
|| (Z_STRLEN_P(op)==1 && Z_STRVAL_P(op)[0]=='0')) {
Z_LVAL_P(op) = 0;
} else {
Z_LVAL_P(op) = 1;
}
STR_FREE(strval);
}
break;
case IS_ARRAY:
tmp = (zend_hash_num_elements(Z_ARRVAL_P(op))?1:0);
zval_dtor(op);
Z_LVAL_P(op) = tmp;
break;
case IS_OBJECT:
{
zend_bool retval = 1;
TSRMLS_FETCH();
convert_object_to_type(op, IS_BOOL, convert_to_boolean);
if (Z_TYPE_P(op) == IS_BOOL) {
return;
}
zval_dtor(op);
ZVAL_BOOL(op, retval);
break;
}
default:
zval_dtor(op);
Z_LVAL_P(op) = 0;
break;
}
Z_TYPE_P(op) = IS_BOOL;
}
/* }}} */
case IS_STRING這段代碼即是將一個字串類型變數轉換為布爾型的操作。可以看到,只有Null 字元串,或者字串長度為1,並且此字元為0時,字串的布爾值才為1,也就是true,其他為0,也就是false。
同樣的,我們也就明白了布爾值如何轉換為字串的,可以從_convert_to_string函數的實現中瞭解。
看似簡單並且基礎的PHP問題,究其根源是對PHP實現機制的把握。個人覺得,這道題也不失為鑒別PHPer知識邊界的一道好題目。