原文鏈接:http://www.orlion.ga/1237/
類的成員變量在PHP中本質是一個變量,只是這些變量都歸屬於某個類,並且給這些變量是有訪問控制的。
類的成員方法在PHP中本質是一個函數,只是這個函數以類的方法存在,他可能是一個類方法也可能是一個實例方法,並且在這些方法都加上了類的訪問控制。類的成員方法是現實世界實體行為的抽象,可以用來實現類的行為。
一、成員變量
成員變量在編譯時已經注冊到了類的結構中。在編譯時類的聲明編譯會調用zend_do_begin_class_declaration函數。此函數用來初始化類的基本信息,其中包括類的成員變量。其調用順序為[zend_do_begin_class_declaration]–>[zend_initalize_class_data]–>[zend_hash_init_ex]
zend_hash_init_ex(&ce->default_properties, 0, NULL, zval_ptr_dtor_func, persistent_hashes, 0);
因為類的成員變量是保存在HashTable,所以其數據的初始化使用zend_hash_init_ex函數來進行。
在聲明類的時候初始化了類的成員變量所在的HashTable,之後如果有新的成員變量屬性聲明時,在編譯時zend_do_declare_property。函數首先檢查成員變量不允許的一些情況:
接口中不允許使用成員變量
成員變量不能擁有抽象屬性
不能聲明成員變量為final
不能重復聲明屬性
如果在類中將一個屬性聲明為final:
public final $var
會報錯:Fatal error: Cannot declare property …這個錯誤由zend_do_declare_property函數拋出:
if (access_type & ZEND_ACC_FINAL) { zend_error(E_COMPILE_ERROR, "Cannot declare property %s::$%s final, the final modifier is allowed only for methods and classes", CG(active_class_entry)->name, var_name- >u.constant.value.str.val); }
在定義檢查沒有問題之後,函數會進行成員變量的初始化操作。
ALLOC_ZVAL(property); // ¾ĘŴ if (value) { // ÒʻĻļUɩ ȐďĤ *property = value->u.constant; } else { INIT_PZVAL(property); Z_TYPE_P(property) = IS_NULL; }
在初始化過程中,程序會先分配內存,如果這個成員變量有初始化的數據,則將數據直接賦值給該屬性,否則初始化ZVAL,並將其類型設置為IS_NULL。在初始化過程完成後,程序通過調用zend_declare_property_ex函數將此成員變量添加到指定的類結構中。
常規的成員變量最後都會注冊到類的default_propertiles字段。在我們平時的工作中,可能會用不到上面所說的這些過程,但是我們可能會使用get_class_vars()函數來查看類的成員變量。此函數返回由類的默認屬性組成的關聯數組,這個數組的元素以varname=>value的形式存在。其實現核心代碼如下:
if (zend_lookup_class(class_name, class_name_len, &pce TSRMLS_CC) == FAILURE) { RETURN_FALSE; } else { array_init(return_value); zend_update_class_constants(*pce TSRMLS_CC); add_class_vars(*pce, &(*pce)->default_properties, return_value TSRMLS_CC); add_class_vars(*pce, CE_STATIC_MEMBERS(*pce), return_value TSRMLS_CC); }
首先調用zend_lookup_class函數查找名為class_name的類,並將復制給pce變量。這個查找的過程最核心是一個HashTable的查找函數zend_hash_quick_find,它會查找EG(class_table)。判斷類是否存在,如果存在則直接返回。如果不存在,則需要判斷是否可以自動加載,如果可以自動加載,則會加載類後再返回。如果不能找到類,則返回FALSE。如果找到了類,則初始化返回的數組,更新類的靜態成員變量,添加類的成員變量到返回的數組。這裡針對類的靜態成員變量有一個更新的過程,關於這個過程我們在下面有關於靜態變量中做相關介紹。
二、靜態成員變量
類的靜態成員變量是所有實例公用的,它歸屬於這個類,因此它也叫做類變量。在PHP的類結構中,類本身的靜態變量存在在類結構的default_static_memebers字段中。
與普通成員變量不同,類變量可以直接通過類名調用,這也體現其稱作類變量的特別。一個PHP實例:
class Tipi { public static $var = 10; } Tipi::$var;
通過VLD擴展查看其生成的中間代碼:
function name: (null) number of ops: 6 compiled vars: !0 = $var line # * op fetch ext return operands ------------------------------------------------------------------------------- - - 2 0 > EXT_STMT 1 NOP 6 2 EXT_STMT 3 ZEND_FETCH_CLASS :1 'Tipi' 4 FETCH_R static member 'var' 5 > RETURN 1 branch: # 0; line: 2- 6; sop: 0; eop: 5 path #1: 0, Class Tipi: [no user functions]
這段中間代碼僅僅與Tipi::$var這段調用對應,它與前面的類定義沒有多大關系。根據VLD生成的內容我們可以知道PHP代碼:Tipi::$var,生成的中間代碼包括ZEND_FETCH_CLASS和FETCH_R。這裡只是一個靜態變量的調用,但是它卻生成了兩個中間代碼。原因:我們要調用一個類的靜態變量,當然要先找到這個類,然後再獲取這個類的變量。從PHP源碼來看,這是由於在編譯時其調用了zend_do_fetch_static_member函數,而在此函數中又調用了zend_do_fetch_class函數,從而會生成ZEND_FETCH_CLASS中間代碼。它所對應的執行函數為ZEND_FETCH_CLASS_SPEC_CONST_HANDLER。此函數會調用zend_fetch_class函數(Zend/zend_execute_API.c)。而zend_fetch_class函數最終也會調用zend_lookup_class_ex函數查找類。
找到了類接著應該就是查找類的靜態成員變量,其最終調用的函數為:zend_std_get_static_property。這裡由於第二個參數的類型為ZEND_FETCH_STATIC_MEMBER。這個函數最後是從static_members字段中查找對應的值返回。而在查找前會和前面一樣,執行zend_update_class_constant函數,從而更新此類的所有靜態成員變量,靜態變量更新流程圖:
三、成員方法
成員方法從本質上來將也是一種函數,所以其存儲結構也和常規函數一樣,存儲在zend_function結構體中。對於一個類的多個成員方法,它是以HashTable的數據結構存儲了多個zend_function結構體。和前面的成員變量一樣,在類聲明時成員方法也通過調用zend_initalize_class_data方法,初始化了整個方法列表所在的HashTable。
除去訪問控制關鍵字,一個成員方法和常規函數是一樣的,從語法解析中調用的函數一樣(都是zend_do_begin_function_declaration函數),但是其調用的參數有一些不同,第三個參數is_method,成員方法的賦值為1,表示它作為成員方法的屬性。在這個函數中會有一系統的編譯判斷,比如在接口中不能聲明私有的成員方法。
在此程序判斷後,程序將方法直接添加到類結構的function_table字段,在此之後,又是若干的編譯檢測。比如接口的一些魔術方法不能設置為非公有,不能被設置為static,如__call()、__callStatic()、__get()等。
與成員變量一樣,成員方法也有一個返回所有成員方法的函數–get_class_methods()。此函數返回由指定的類中定義的方法名所組成的數組。
四、靜態成員方法
類的靜態成員方法通常也叫做類方法。與靜態成員變量不同,靜態成員方法與成員方法都存儲在類結構的function_table字段。
class Tipi{ public static function t() { echo 1; } } Tipi::t();
以上的代碼在VLD擴展下生成的部分中間代碼:
number of ops: 8 compiled vars: none line # * op fetch ext return operands ------------------------------------------------------------------------------- -- 2 0 > EXT_STMT 1 NOP 8 2 EXT_STMT 3 ZEND_INIT_STATIC_METHOD_CALL 'Tipi','t' 4 EXT_FCALL_BEGIN 5 DO_FCALL_BY_NAME 0 6 EXT_FCALL_END 9 7 > RETURN 1 branch: # 0; line: 2- 9; sop: 0; eop: 7 path #1: 0, Class Tipi: Function t: Finding entry points Branch analysis from position: 0
從以上的內容可以看出整個靜態成員方法的調用是一個先查找方法再調用的過程。而對於調用操作,對應的中間代碼為ZEND_INIT_STATIC_METHOD_CALL。由於類名和方法名都是常量,於是我們可以知道中間代碼對應的函數是ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER。在這個函數中,它會首先調用zend_fetch_class函數,通過類名在EG(class_table)中查找類,然後再執行靜態方法的獲取方法。
if (ce->get_static_method) { EX(fbc) = ce->get_static_method(ce, function_name_strval, function_name_strlen TSRMLS_CC); } else { EX(fbc) = zend_std_get_static_method(ce, function_name_strval, function_name_strlen TSRMLS_CC); }
如果類結構中的get_static_method方法存在,則調用此方法,如果不存在,則調用zend_std_get_static_method。在PHP的源碼中get_static_method方法一般都是NULL,這裡我們重點查看zend_std_get_static_method函數。此函數會查找ce->function_table列表,在查找到方法後檢查方法的訪問控制權限,如果不允許訪問,則報錯,否則返回函數結構體。