天天看點

六十八、PHP核心探索:類的成員變量 ☞ 成員變量是定義在類裡面

在上一小節,我們介紹了類的結構和聲明過程,進而,我們知道了類的存儲結構,接口抽象類等類型的實作方式。 在本小節,我們将介紹類的成員變量和成員方法。首先,我們看一下,什麼是成員變量,什麼是成員方法。

類的成員變量在PHP中本質上是一個變量,隻是這些變量都歸屬于某個類,并且給這些變量是有通路控制的。 類的成員變量也稱為成員屬性,它是現實世界實體屬性的抽象,是可以用來描述對象狀态的資料。

類的成員方法在PHP中本質上是一個函數,隻是這個函數以類的方法存在,它可能是一個類方法也可能是一個執行個體方法, 并且在這些方法上都加上了類的通路控制。類的成員方法是現實世界實體行為的抽象,可以用來實作類的行為。

成員變量

前面介紹過變量,不過那些變量要麼是定義在全局範圍中,叫做全局變量,要麼是定義在某個函數中, 叫做局部變量。 成員變量是定義在類裡面,并和成員方法處于同一層次。如下一個簡單的PHP代碼示例,定義了一個類, 并且這個類有一個成員變量。

class Tipi {
    public $var;
}      

類的結構在PHP核心中的存儲方式我們已經在上一小節介紹過了。現在,我們要讨論類的成員變量的存儲方式。 假如我們需要直接通路這個變量,整個通路過程是什麼? 當然,以這個示例來說,通路這個成員變量是通過對象來通路,關于對象的相關知識我們将在後面的小節作詳細的介紹。

當我們用VLD擴充檢視以上代碼生成的中間代碼時,我們發現,并沒有相關的中間代碼輸出。 這是因為成員變量在編譯時已經注冊到了類的結構中,那注冊的過程是什麼? 成員變量注冊的位置在哪?

我們從上一小節知道,在編譯時類的聲明編譯會調用zend_do_begin_class_declaration函數。 此函數用來初始化類的基本資訊,其中包括類的成員變量。其調用順序為: [zend_do_begin_class_declaration] --> [zend_initialize_class_data] --> [zend_hash_init_ex]

zend_hash_init_ex(&ce->default_properties, 0, NULL, zval_ptr_dtor_func, persistent_hashes, 0);      

在聲明類的時候初始化了類的成員變量所在的HashTable,之後如果有新的成員變量聲明時,在編譯時zend_do_declare_property。函數首先檢查成員變量不允許的一些情況:

  • 接口中不允許使用成員變量
  • 成員變量不能擁有抽象屬性
  • 不能聲明成員變量為final
  • 不能重複聲明屬性

如果在上面的PHP代碼中的類定義中,給成員變量前面添加final關鍵字:

class Tipi {
    public final $var;
}      

運作程式将報錯:Fatal error: Cannot declare property Tipi::$var final, the final modifier is allowed only for methods and classes in .. 這個錯誤由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) {    //  成員變量有初始化資料
    *property = value->u.constant;
} else {
    INIT_PZVAL(property);
    Z_TYPE_P(property) = IS_NULL;
}      

在初始化過程中,程式會先配置設定記憶體,如果這個成員變量有初始化的資料,則将資料直接指派給該屬性, 否則初始化ZVAL,并将其類型設定為IS_NULL。在初始化過程完成後,程式通過調用 zend_declare_property_ex 函數将此成員變量添加到指定的類結構中。

以上為成員變量的初始化和注冊成員變量的過程,正常的成員變量最後都會注冊到類的 default_properties 字段。 在我們平時的工作中,可能會用不到上面所說的這些過程,但是我們可能會使用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_members 字段中。

與普通成員變量不同,類變量可以直接通過類名調用,這也展現其稱作類變量的特别。一個PHP示例:

class Tipi {
    public static $var = 10;
}
 
Tipi::$var;      

這是一個簡單的類,它僅包括一個公有的靜态變量$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_constants函數,進而更新此類的所有靜态成員變量,其程式流程如圖所示:

​​

​​