天天看點

六十九、PHP核心探索:類的成員方法 ☞ 成員方法從本質上來講也是一種函數

成員方法從本質上來講也是一種函數,是以其存儲結構也和正常函數一樣,存儲在zend_function結構體中。 對于一個類的多個成員方法,它是以HashTable的資料結構存儲了多個zend_function結構體。 和前面的成員變量一樣,在類聲明時成員方法也通過調用zend_initialize_class_data方法,初始化了整個方法清單所在的HashTable。 在類中我們如果要定義一個成員方法,格式如下:

class Tipi{
    public function t() {
        echo 1;
    }
}      

除去通路控制關鍵字,一個成員方法和正常函數是一樣的,從文法解析中調用的函數一樣(都是zend_do_begin_function_declaration函數), 但是其調用的參數有一些不同,第三個參數is_method,成員方法的指派為1,表示它作為成員方法的屬性。 在這個函數中會有一系統的編譯判斷,比如在接口中不能聲明私有的成員方法。 看這樣一段代碼:

interface Ifce {
   private function method();
}      

如果直接運作,程式會報錯:Fatal error: Access type for interface method Ifce::method() must be omitted in 這段代碼對應到zend_do_begin_function_declaration函數中的代碼,如下:

if (is_method) {
    if (CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE) {
        if ((Z_LVAL(fn_flags_znode->u.constant) & ~(ZEND_ACC_STATIC|ZEND_ACC_PUBLIC))) {
            zend_error(E_COMPILE_ERROR, "Access type for interface method %s::%s() must be omitted",
                CG(active_class_entry)->name, function_name->u.constant.value.str.val);
        }
        Z_LVAL(fn_flags_znode->u.constant) |= ZEND_ACC_ABSTRACT; /* propagates to the rest of the parser */
    }
    fn_flags = Z_LVAL(fn_flags_znode->u.constant); /* must be done *after* the above check */
} else {
    fn_flags = 0;
}      

在此程式判斷後,程式将方法直接添加到類結構的function_talbe字段,在此之後,又是若幹的編譯檢測。 比如接口的一些魔術方法不能被設定為非公有,不能被設定為static,如__call()、__callStatic()、__get()等。 如果在接口中設定了靜态方法,如下定義的一個接口:

interface ifce {
    public static function __get();
}      

若運作這段代碼,則會顯示Warning:Warning: The magic method __get() must have public visibility and cannot be static in

這段編譯檢測在zend_do_begin_function_declaration函數中對應的源碼如下:

if (CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE) {
        if ((name_len == sizeof(ZEND_CALL_FUNC_NAME)-1) && (!memcmp(lcname, ZEND_CALL_FUNC_NAME, sizeof(ZEND_CALL_FUNC_NAME)-1))) {
            if (fn_flags & ((ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC) ^ ZEND_ACC_PUBLIC)) {
                zend_error(E_WARNING, "The magic method __call() must have public visibility and cannot be static");
            }
        } else if() {   //  其它魔術方法的編譯檢測
        }
}      

同樣,對于類中的這些魔術方法,也有同樣的限制,如果在類中定義了靜态的魔術方法,則顯示警告。如下代碼:

class Tipi {
    public static function __get($var) {
 
    }
}      

運作這段代碼,則會顯示: Warning: The magic method __get() must have public visibility and cannot be static in

與成員變量一樣,成員方法也有一個傳回所有成員方法的函數--get_class_methods()。 此函數傳回由指定的類中定義的方法名所組成的數組。 從 PHP 4.0.6 開始,可以指定對象本身來代替指定的類名。 它屬于PHP内建函數,整個程式流程就是一個周遊類成員方法清單,判斷是否為符合條件的方法, 如果是,則将這個方法作為一個元素添加到傳回數組中。

靜态成員方法

類的靜态成員方法通常也叫做類方法。 與靜态成員變量不同,靜态成員方法與成員方法都存儲在類結構的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清單,在查找到方法後檢查方法的通路控制權限,如果不允許通路,則報錯,否則傳回函數結構體。 關于通路控制,我們在後面的小節中說明。

靜态方法和執行個體方法的小漏洞

細心的讀者應該注意到前面提到靜态方法和執行個體方法都是儲存在類結構體zend_class_entry.function_table中,那這樣的話, Zend引擎在調用的時候是怎麼區分這兩類方法的,比如我們靜态調用執行個體方法或者執行個體調用靜态方法會怎麼樣呢?

可能一般人不會這麼做,不過筆者有一次錯誤的這樣調用了,而代碼沒有出現任何問題, 在review代碼的時候意外發現筆者像執行個體方法那樣調用的靜态方法,而什麼問題都沒有發生(沒有報錯)。 在理論上這種情況是不應發生的,類似這這樣的情況在PHP中是非常的多的,例如前面提到的create_function方法傳回的僞匿名方法, 後面介紹通路控制時還會介紹通路控制的一些瑕疵,PHP在現實中通常采用Quick and Dirty的方式來實作功能和解決問題, 這一點和Ruby完整的面向對象形成鮮明的對比。我們先看一個例子:

<?php
 
error_reporting(E_ALL);
 
class A {
    public static function staticFunc() {
        echo "static";
    }
 
    public function instanceFunc() {
        echo "instance";    
    }
}
 
A::instanceFunc(); // instance
$a = new A();
$a->staticFunc();  // static
?>      

上面的代碼靜态的調用了執行個體方法,程式輸出了instance,執行個體調用靜态方法也會正确輸出static,這說明這兩種方法本質上并沒有卻别。 唯一不同的是他們被調用的上下文環境,例如通過執行個體方法調用方法則上下文中将會有$this這個特殊變量,而在靜态調用中将無法使用$this變量。

不過實際上Zend引擎是考慮過這個問題的,将error_reporting的級别增加E_STRICT,将會出出現E_STRICT錯誤:

Strict Standards: Non-static method A::instanceFunc() should not be called statically