PHP是一門簡單而強大的語言,提供了很多Web適用的語言特性,其中就包括了變數弱類型,在弱類型機制下,你能夠給一個變數賦任意類型的值。
PHP的執行是通過Zend Engine(下面簡稱ZE),ZE是使用C編寫,在底層實現了一套弱類型機制。ZE的記憶體管理使用寫時拷貝、引用計數等最佳化策略,減少再變數賦值時候的記憶體拷貝。
下面不光帶你探索PHP弱類型的原理,也會在寫PHP擴充角度,介紹如何操作PHP的變數。
1. PHP的變數類型
PHP的變數類型有8種:
- 標準類型:布爾boolen,整型integer,浮點float,字元string
- 複雜類型:數組array,對象object
- 特殊類型:資源resource
PHP不會嚴格檢驗變數類型,變數可以不顯示的聲明其類型,而在運行期間直接賦值。也可以將變數自由的轉換類型。如下例,沒有實現聲明的情況下,$i可以賦任意類型的值。
[php] view plaincopy
- $i = 1; //int $i = 'show me the money'; //string $i = 0.02; // float $i = array(1, 2, 3); // array $i = new Exception('test', 123); // object $i = fopen('/tmp/aaa.txt', 'a') // resource ?>
如果你對弱類型原理理解不深刻,在變數比較時候,會出現“超出預期”的驚喜。
[php] view plaincopy
- $str1 = null; $str2 = false; echo $str1==$str2 ? '相等' : '不相等'; $str3 = ''; $str4 = 0; echo $str3==$str4 ? '相等' : '不相等'; $str5 = 0; $str6 = '0'; echo $str5==$str6 ? '相等' : '不相等'; ?>
以上三個結果全部是相等,因為在變數比較的時候,PHP內部做了變數轉換。如果希望值和類型同時判斷,請使用三個=(如,$a===0)來判斷。也許你會覺得司空見慣,也許你會覺得很神奇,那麼請跟我一起深入PHP核心,探索PHP變數原理。
2. 變數的儲存及標準類型介紹
PHP的所有變數,都是以結構體zval來實現,在Zend/zend.h中我們能看到zval的定義:
[php] view plaincopy
- typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; /* this will always be set for strings */ } str; /* string (always has length) */ HashTable *ht; /* an array */ zend_object_value obj; /* stores an object store handle, and handlers */ } zvalue_value;
屬性名稱 |
含義 |
預設值 |
refcount__gc |
表示引用計數 |
1 |
is_ref__gc |
表示是否為引用 |
0 |
value |
儲存變數的值 |
|
type |
變數具體的類型 |
|
其中refcount__gc和is_ref__gc表示變數是否是一個引用。type欄位標識變數的類型,type的值可以是:IS_NULL,IS_BOOL,IS_LONG,IS_FLOAT,IS_STRING,IS_ARRAY,IS_OBJECT,IS_RESOURCE。PHP根據type的類型,來選擇如何儲存到zvalue_value。
zvalue_value能夠實現變數弱類型的核心,定義如下:
[php] view plaincopy
- typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; /* this will always be set for strings */ } str; /* string (always has length) */ HashTable *ht; /* an array */ zend_object_value obj; /* stores an object store handle, and handlers */ } zvalue_value;
布爾型,zval.type=IS_BOOL,會讀取zval.value.lval欄位,值為1/0。如果是字串,zval.type=IS_STRING,會讀取zval.value.str,這是一個結構體,儲存了字串指標和長度。
C語言中,用"\0"作為字串結束符。也就是說一個字串"Hello\0World"在C語言中,用printf來輸出的話,只能輸出hello,因為"\0"會認為字元已經結束。PHP中是通過結構體的_zval_value.str.len來控制字元串長度,相關函數不會遇到"\0"結束。所以PHP的字串是二進位安全的。
如果是NULL,只需要zval.type=IS_NULL,不需要讀取值。
通過對zval的封裝,PHP實現了弱類型,對於ZE來說,通過zval可以存取任何類型。
3. 進階類型Array和Object數組Array
數組是PHP語言中非常強大的一個資料結構,分為索引數組和關聯陣列,zval.type=IS_ARRAY。在關聯陣列中每個key可以儲存任意類型的資料。PHP的數組是用Hash Table實現的,數組的值存在zval.value.ht中。
後面會專門講到PHP雜湊表的實現。
物件類型的zval.type=IS_OBJECT,值存在zval.value.obj中。
4. 特殊類型——資源類型(Resource)介紹
資源類型是個很特殊的類型,zval.type=IS_RESOURCE,在PHP中有一些很難用常規類型描述的資料結構,比如檔案控制代碼,對於C語言來說是一個指標,不過PHP中沒有指標的概念,也不能用常規類型來約束,因此PHP通過資源類型概念,把C語言中類似檔案指標的變數,用zval結構來封裝。資源類型值是一個整數,ZE會根據這個值去資源的雜湊表中擷取。
資源類型的定義:
[php] view plaincopy
- typedefstruct_zend_rsrc_list_entry { void *ptr; int type; int refcount; }zend_rsrc_list_entry;
其中,ptr是一個指向資源的最終實現的指標,例如一個檔案控制代碼,或者一個資料庫連接結構。type是一個類型標記,用於區分不同的資源類型。refcount用於資源的引用計數。
核心中,資源類型是通過函數ZEND_FETCH_RESOURCE擷取的。
[php] view plaincopy
- ZEND_FETCH_RESOURCE(con, type, zval *, default, resource_name, resource_type);
5. 變數類型的轉換
按照現在我們對PHP語言的瞭解,變數的類型依賴於zval.type欄位指示,變數的內容按照zval.type儲存到zval.value。當PHP中需要變數的時候,只需要兩個步驟:把zval.value的值或指標改變,再改變zval.type的類型。不過對於PHP的一些進階變數Array/Object/Resource,變數轉換要進行更多操作。
變數轉換原理分為3種:
5.1 標準類型相互轉換
比較簡單,按照上述的步驟轉化即可。
5.2 標準類型與資源類型轉換
資源類型可以理解為是int,比較方便轉換標準類型。轉換後資源會被close或回收。
[php] view plaincopy
- $var = fopen('/tmp/aaa.txt', 'a'); // 資源 #1 $var = (int) $var; var_dump($var); // 輸出1 ?>
5.3 標準類型與複雜類型轉換
Array轉換整型int/浮點型float會返回元素個數;轉換bool返回Array中是否有元素;轉換成string返回'Array',並拋出warning。
詳細內容取決於經驗,請閱讀PHP手冊: http://php.net/manual/en/language.types.type-juggling.php
5.4 複雜類型相互轉換
array和object可以互轉。如果其它任何類型的值被轉換成對象,將會建立一個內建類stdClass的執行個體。
在我們寫PHP擴充的時候,PHP核心提供了一組函數用於類型轉換:
void convert_to_long(zval* pzval) |
void convert_to_double(zval* pzval) |
void convert_to_long_base(zval* pzval, int base) |
void convert_to_null(zval* pzval) |
void convert_to_boolean(zval* pzval) |
void convert_to_array(zval* pzval) |
void convert_to_object(zval* pzval) |
void convert_object_to_type(zval* pzval, convert_func_t converter) |
PHP核心提供的一組宏來方便的訪問zval,用於更細粒度的擷取zval的值:
核心訪問zval容器的API |
宏 |
訪問變數 |
Z_LVAL(zval) |
(zval).value.lval |
Z_DVAL(zval) |
(zval).value.dval |
Z_STRVAL(zval) |
(zval).value.str.val |
Z_STRLEN(zval) |
(zval).value.str.len |
Z_ARRVAL(zval) |
(zval). value.ht |
Z_TYPE(zval) |
(zval).type |
Z_LVAL_P(zval) |
(*zval).value.lval |
Z_DVAL_P(zval) |
(*zval).value.dval |
Z_STRVAL_P(zval_p) |
(*zval).value.str.val |
Z_STRLEN_P(zval_p) |
(*zval).value.str.len |
Z_ARRVAL_P(zval_p) |
(*zval). value.ht |
Z_OBJ_HT_P(zval_p) |
(*zval).value.obj.handlers |
Z_LVAL_PP(zval_pp) |
(**zval).value.lval |
Z_DVAL_PP(zval_pp) |
(**zval).value.dval |
Z_STRVAL_PP(zval_pp) |
(**zval).value.str.val |
Z_STRLEN_PP(zval_pp) |
(**zval).value.str.len |
Z_ARRVAL_PP(zval_pp) |
(**zval). value.ht |
6. 變數的符號表與範圍
PHP的變數符號表與zval值的映射,是通過HashTable(雜湊表,又叫做散列表,下面簡稱HT),HashTable在ZE中廣泛使用,包括常量、變數、函數等語言特性都是HT來組織,在PHP的數群組類型也是通過HashTable來實現。
舉個例子:
[php] view plaincopy
- $var = 'Hello World'; ?>
$var的變數名會儲存在變數符號表中,代表$var的類型和值的zval結構儲存在雜湊表中。核心通過變數符號表與zval地址的雜湊映射,來實現PHP變數的存取。
為什麼要提範圍呢?因為函數內部變數保護。按照範圍PHP的變數分為全域變數和局部變數,每種範圍PHP都會維護一個符號表的HashTable。當在PHP中建立一個函數或類的時候,ZE會建立一個新的符號表,表明函數或類中的變數是局部變數,這樣就實現了局部變數的保護--外部無法訪問函數內部的變數。當建立一個PHP變數的時候,ZE會分配一個zval,並設定相應type和初始值,把這個變數加入當前範圍的符號表,這樣使用者才能使用這個變數。
核心中使用ZEND_SET_SYMBOL來設定變數:
[php] view plaincopy
- ZEND_SET_SYMBOL( EG(active_symbol_table), "foo", foo);
查看_zend_executor_globals結構
[php] view plaincopy
- Zend/zend_globals.h
- struct _zend_executor_globals { //略 HashTable symbol_table;//全域變數的符號表 HashTable *active_symbol_table;//局部變數的符號表 //略 };
在寫PHP擴充時候,可以通過EG宏來訪問PHP的變數符號表。EG(symbol_table)訪問全域範圍的變數符號表,EG(active_symbol_table)訪問當前範圍的變數符號表,局部變數儲存的是指標,在對HashTable進行操作的時候傳遞給相應函數。
為了更好的理解變數的雜湊表與範圍,舉個簡單的例子:
[php] view plaincopy
- $temp = 'global'; function test() { $temp = 'active'; } test(); var_dump($temp); ?>
建立函數外的變數$temp,會把這個它加入全域符號表,同時在全域符號表的HashTable中,分配一個字元類型的zval,值為‘global‘。建立函數test內部變數$temp,會把它加入屬於函數test的符號表,分配字元型zval,值為’active' 。
7. PHP擴充中變數操作
建立PHP變數
我們可以在擴充中調用函數MAKE_STD_ZVAL(pzv)來建立一個PHP可調用的變數,MAKE_STD_ZVAL應用到的宏有:
[php] view plaincopy
- #define MAKE_STD_ZVAL(zv) ALLOC_ZVAL(zv);INIT_PZVAL(zv) #define ALLOC_ZVAL(z) ZEND_FAST_ALLOC(z, zval, ZVAL_CACHE_LIST) #define ZEND_FAST_ALLOC(p, type, fc_type) (p) = (type *) emalloc(sizeof(type)) #define INIT_PZVAL(z) (z)->refcount__gc = 1;(z)->is_ref__gc = 0;
MAKE_STD_ZVAL(foo)展開後得到:
[php] view plaincopy
- (foo) = (zval *) emalloc(sizeof(zval)); (foo)->refcount__gc = 1; (foo)->is_ref__gc = 0;
可以看出,MAKE_STD_ZVAL做了三件事:分配記憶體、初始化zval結構中的refcount、is_ref。
核心中提供一些宏來簡化我們的操作,可以只用一步便設定好zval的類型和值。
API Macros for Accessing zval |
宏 |
實現方法 |
ZVAL_NULL(pvz) |
Z_TYPE_P(pzv) = IS_NULL |
ZVAL_BOOL(pvz) |
Z_TYPE_P(pzv) = IS_BOOL; Z_BVAL_P(pzv) = b ? 1 : 0; |
ZVAL_TRUE(pvz) |
ZVAL_BOOL(pzv, 1); |
ZVAL_FALSE(pvz) |
ZVAL_BOOL(pzv, 0); |
ZVAL_LONG(pvz, l)(l 是值) |
Z_TYPE_P(pzv) = IS_LONG;Z_LVAL_P(pzv) = l; |
ZVAL_DOUBLE(pvz, d) |
Z_TYPE_P(pzv) = IS_DOUBLE;Z_LVAL_P(pzv) = d; |
ZVAL_STRINGL(pvz, str, len, dup) |
Z_TYPE_P(pzv) = IS_STRING;Z_STRLEN_P(pzv) = len; if (dup) { {Z_STRVAL_P(pzv) =estrndup(str, len + 1);} }else { {Z_STRVAL_P(pzv) = str;} } |
ZVAL_STRING(pvz, str, len) |
ZVAL_STRINGL(pzv, str,strlen(str), dup); |
ZVAL_RESOURCE(pvz, res) |
Z_TYPE_P(pzv) = IS_RESOURCE;Z_RESVAL_P(pzv) = res; |
ZVAL_STRINGL(pzv,str,len,dup)中的dup參數
先闡述一下ZVAL_STRINGL(pzv,str,len,dup); str和len兩個參數很好理解,因為我們知道核心中儲存了字串的地址和它的長度,後面的dup的意思其實很簡單,它指明了該字串是否需要被複製。值為 1 將先申請一塊新記憶體並賦值該字串,然後把新記憶體的地址複製給pzv,為 0 時則是直接把str的地址賦值給zval。
ZVAL_STRINGL與ZVAL_STRING的區別
如果你想在某一位置截取該字串或已經知道了這個字串的長度,那麼可以使用宏 ZVAL_STRINGL(zval, string, length, duplicate) ,它顯式的指定字串長度,而不是使用strlen()。這個宏該字串長度作為參數。但它是二進位安全的,而且速度也比ZVAL_STRING快,因為少了個strlen。
ZVAL_RESOURCE約等於ZVAL_LONG
在章節4中我們說過,PHP中的資源類型的值是一個整數,所以ZVAL_RESOURCE和ZVAL_LONG的工作差不多,只不過它會把zval的類型設定為 IS_RESOURCE。
8. 總結
PHP的弱類型是通過ZE的zval容器轉換完成,通過雜湊表來儲存變數名和zval資料,在運行效率方面有一定犧牲。另外因為變數類型的隱性轉換,在開發過程中對變數類型檢測力度不夠,可能會導致問題出現。
不過PHP的弱類型、數組、記憶體託管、擴充等語言特性,非常適合Web開發情境,開發效率很高,能夠加快產品迭代周期。在海量服務中,通常瓶頸存在於資料訪問層,而不是語言本身。在實際使用PHP不僅擔任邏輯層和展現層的任務,我們甚至用PHP開發的UDPServer/TCPServer作為資料和cache的中介層。
以上就介紹了王帥:深入PHP核心(一)——弱類型變數原理探究,包括了方面的內容,希望對PHP教程有興趣的朋友有所協助。