面向對象的三大特性(封裝、繼承、多態),在前一小節介紹了封裝,這一小節我們將介紹繼承和多態的實現。
## 繼承[]()
繼承是一種關聯類的層次模型,它可以建立類之間的關系,并實現代碼重用,方便系統擴展。繼承提供了一種明確表述共性的方法,是一個新類從現有的類中派生的過程。繼承產生的新類繼承了原始類的特性,新類稱為原始類的派生類(或子類),而原始類稱為新類的基類(或父類)。派生類可以從基類那里繼承方法和變量,并且新類可以重載或增加新的方法,使之滿足自己的定制化的需要。
PHP中使用extends關鍵字來進行類的繼承,一個類只能繼承一個父類。被繼承的成員方法和成員變量可以使用同名的方法或變量重寫,如果需要訪問父類的成員方法或變量可以使用特殊類parent來進行。
PHP內核將類的繼承實現放在了"編譯階段",因此使用VLD生成中間代碼時會發現并沒有關于繼承的相關信息。通過對extends關鍵字的詞法分析和語法分析,在Zend/zend_complie.c文件中找到繼承實現的編譯函數zend_do_inheritance()。其調用順序如下: [zend_do_early_binding] --> [do_bind_inherited_class()] --> [zend_do_inheritance()]
ZEND_API void zend_do_inheritance(zend_class_entry *ce, zend_class_entry *parent_ce TSRMLS_DC)
{
// ...省略 報錯處理 接口不能從類繼承,final類不能繼承
?
// ...省略 序列化函數和反序列化函數 如果當前類沒有,則取父類的
?
/* Inherit interfaces */
zend_do_inherit_interfaces(ce, parent_ce TSRMLS_CC);
?
/* Inherit properties */
zend_hash_merge(&ce->default_properties, &parent_ce->default_properties, (void (*)(void *)) zval_add_ref, NULL, sizeof(zval *), 0);
if (parent_ce->type != ce->type) {
/* User class extends internal class */
zend_update_class_constants(parent_ce TSRMLS_CC);
zend_hash_apply_with_arguments(CE_STATIC_MEMBERS(parent_ce) TSRMLS_CC, (apply_func_args_t)inherit_static_prop, 1, &ce->default_static_members);
} else {
zend_hash_apply_with_arguments(&parent_ce->default_static_members TSRMLS_CC, (apply_func_args_t)inherit_static_prop, 1, &ce->default_static_members);
}
zend_hash_merge_ex(&ce->properties_info, &parent_ce->properties_info, (copy_ctor_func_t) (ce->type & ZEND_INTERNAL_CLASS ? zend_duplicate_property_info_internal : zend_duplicate_property_info), sizeof(zend_property_info), (merge_checker_func_t) do_inherit_property_access_check, ce);
?
zend_hash_merge(&ce->constants_table, &parent_ce->constants_table, (void (*)(void *)) zval_add_ref, NULL, sizeof(zval *), 0);
zend_hash_merge_ex(&ce->function_table, &parent_ce->function_table, (copy_ctor_func_t) do_inherit_method, sizeof(zend_function), (merge_checker_func_t) do_inherit_method_check, ce);
do_inherit_parent_constructor(ce);
?
if (ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS && ce->type == ZEND_INTERNAL_CLASS) {
ce->ce_flags |= ZEND_ACC_EXPLICIT_ABSTRACT_CLASS;
} else if (!(ce->ce_flags & ZEND_ACC_IMPLEMENT_INTERFACES)) {
/* The verification will be done in runtime by ZEND_VERIFY_ABSTRACT_CLASS */
zend_verify_abstract_class(ce TSRMLS_CC);
}
}
整個繼承的過程是以類結構為中心,當繼承發生時,程序會先處理所有的接口。接口繼承調用了zend_do_inherit_interfaces函數此函數會遍歷所有的接口列表,將接口寫入到類結構的interfaces字段,并增加num_interfaces的計數統計。在接口繼承后,程序會合并類的成員變量、屬性、常量、函數等,這些都是HashTable的merge操作。
在繼承過程中,除了常規的函數合并后,還有魔法方法的合并,其調用的函數為do_inherit_parent_constructor(ce)。此函數實現魔術方法繼承,如果子類中沒有相關的魔術方法,則繼承父類的對應方法。如下所示的PHP代碼為子類沒構造函數的情況
class Base {
public function __construct() {
echo 'Base __construct<br />';
}
}
?
class Foo extends Base {
?
}
?
$foo = new Foo();
在PHP函數中運行,會輸出:Base __construct
這顯然繼承了父類的構造方法,如果子類有自己的構造方法,并且需要調用父類的構造方法時需要在子類的構造方法中調用父類的構造方法,PHP不會自動調用。
當說到繼承,就不得不提到訪問控制。繼承在不同的訪問控制權限下有不同的表現。以成員方法為例,我們可以使用private和protected訪問修飾符來控制需要繼承的內容。
- private 如果一個成員被指定為private,它將不能被繼承。實際上在PHP中這個方法會被繼承下來,只是無法訪問。
- protected 如果一個成員被指定為protected,它將在類外不可見,可以被繼承。
在繼承中訪問控制的實現是在合并函數時實現,其實現函數為do_inherit_method_check。在此函數中,如果子類沒有父類中定義的方法,則所有的此類方法都會被繼承,包括私有訪問控制權限的方法。
看一個PHP的示例:
class Base {
private function privateMethod() {
}
}
?
class Child extends Base{
public function publicMethod() {
}
}
?
$c = new Child();
?
if (method_exists($c, 'privateMethod')) {
echo 1;
}else{
echo 0;
}
這段代碼會輸出1,至此,我們可以證明:**在PHP中,對于私有方法,在繼承時是可以被繼承下來的**。
## 多態[]()
多態是繼數據抽象和繼承后的第三個特性。顧名思義,多態即多種形態,相同方法調用實現不同的實現方式。多態關注一個接口或基類,在編程時不必擔心一個對象所屬于的具體類。在面向對象的原則中里氏代換原則(Liskov Substitution Principle,LSP),依賴倒轉原則(dependence inversion principle,DIP)等都依賴于多態特性。而我們在平常工作中也會經常用到。
interface Animal {
public function run();
}
?
class Dog implements Animal {
public function run() {
echo 'dog run';
}
}
?
class Cat implements Animal{
public function run() {
echo 'cat run';
}
}
?
class Context {
private $_animal;
?
public function __construct(Animal $animal) {
$this->_animal = $animal;
}
?
public function run() {
$this->_animal->run();
}
}
?
$dog = new Dog();
$context = new Context($dog);
$context->run();
?
$cat = new Cat();
$context = new Context();
$context->run();
上面是策略模式示例性的簡單實現。對于不同的動物,其跑的方式不一樣,當在環境中跑的時候,根據所傳遞進來的動物執行相對應的跑操作。多態是一種編程的思想,但對于不同的語言,其實現也不同。對于PHP的程序實現來說,關鍵點在于類型提示的實現。而類型提示是PHP5之后才有的特性。在此之前,PHP本身就具有多態特性。
[<< 第三章 第五節 類型提示的實現 >>](#)已經說明了類型提示的實現,只是對于對象的判斷沒有做深入的探討。它已經指出對于類的類型提示實現函數為zend_verify_arg_type。在此函數中,關于對象的關鍵代碼如下:
if (Z_TYPE_P(arg) == IS_OBJECT) {
need_msg = zend_verify_arg_class_kind(cur_arg_info, fetch_type, &class_name, &ce TSRMLS_CC);
if (!ce || !instanceof_function(Z_OBJCE_P(arg), ce TSRMLS_CC)) {
return zend_verify_arg_error(zf, arg_num, cur_arg_info, need_msg, class_name, "instance of ", Z_OBJCE_P(arg)->name TSRMLS_CC);
}
}
第一步,判斷參數是否為對象,使用宏Z_TYPE_P,如果是轉二步,否則跳到其它情況處理
第二步,獲取類的類型驗證信息,調用了zend_verify_arg_class_kind函數,此函數位于Zend/zend_execute.c文件中,它會通過zend_fetch_class函數獲取類信息,根據類的類型判斷是否為接口,返回字符串"implement interface"或"be an instance of"
第三步,判斷是否為指定類的實例,調用的函數是instanceof_function。此函數首先會遍歷實例所在類的所有接口,遞歸調用其本身,判斷實例的接口是否為指定類的實例,如果是,則直接返回1,如果不是,在非僅接口的情況下,循環遍歷所有的父類,判斷父類與指定的類是否相等,如果相等返回1,當函數執行完時仍沒有找到,則返回0,表示不是類的實例。instanceof_function函數的代碼如下:
ZEND_API zend_bool instanceof_function_ex(const zend_class_entry *instance_ce, const zend_class_entry *ce, zend_bool interfaces_only TSRMLS_DC) /* {{{ */
{
zend_uint i;
?
for (i=0; i<instance_ce->num_interfaces; i++) { // 遞歸遍歷所有的接口
if (instanceof_function(instance_ce->interfaces[i], ce TSRMLS_CC)) {
return 1;
}
}
if (!interfaces_only) {
while (instance_ce) { // 遍歷所有的父類
if (instance_ce == ce) {
return 1;
}
instance_ce = instance_ce->parent;
}
}
?
return 0;
}
第四步,如果不是指定類的實例,程序會調用zend_verify_arg_error報錯,此函數最終會調用zend_error函數顯示錯誤。
### 接口的實現[]()
前面的PHP示例中有用到接口,而且在多態中,接口是一個不得不提的概念。接口是一些方法特征的集合,是一種邏輯上的抽象,它沒有方法的實現,因此這些方法可以在不同的地方被實現,可以有相同的名字而具有完全不同的行為。
而PHP內核對類和接口一視同仁,它們的內部結構一樣。這點在前面的類型提示實現中也有看到,不管是接口還是類,調用instanceof_function函數時傳入的參數和計算過程中使用的變量都是zend_class_entry類型。
[<< 第一節 類的結構和實現 >>](#)中已經對于類的類型做了說明,在語法解析時,PHP內核已經設置了其type=ZEND_ACC_INTERFACE,
interface_entry:
T_INTERFACE { $$.u.opline_num = CG(zend_lineno);
$$.u.EA.type = ZEND_ACC_INTERFACE; }
;
而在聲明類的函數zend_do_begin_class_declaration中,通過下列語句,將語法解析的類的類型賦值給類的ce_flags字段。
new_class_entry->ce_flags |= class_token->u.EA.type;
類結構的ce_flags字段的作用是標記類的類型。
接口與類除了在ce_flags字段不同外,在其它一些字段的表現上也不一樣,如繼承時,類只能繼承一個父類,卻可以實現多個接口。二者在類的結構中存儲在不同的字段,類的繼承由于是一對一的關系,則每個類都有一個parent字段。而接口實現是一個一對多的關系,每個類都會有一個二維指針存放接口的列表,還有一個存儲接口數的字段num_interfaces。
接口也可以和類一樣實現繼承,并且只能是一個接口繼承另一個接口。一個類可以實現多個接口,接口在編譯時調用zend_do_implement_interface函數,zend_do_implement_interface函數會合并接口中的常量列表和方法列表操作,這就是接口中不能有變量卻可以有常量的實現原因。在接口繼承的過程中有對當前類的接口中是否存在同樣接口的判斷操作,如果已經存在了同樣的接口,則此接口繼承將不會執行。
## 抽象類[]()
抽象類是相對于具體類來說的,抽象類僅提供一個類的部分實現。抽象類可以有實例變量,構造方法等。抽象類可以同時擁有抽象方法和具體方法。一般來說,抽象類代表一個抽象的概念,它提供了一個繼承的出發點,理想情況下,所有的類都需要從抽象類繼承而來。而具體類則不同,具體類可以實例化。由于抽象類不可以實例化,因此所有抽象類應該都是作為繼承的父類的。
在PHP中,抽象類是被abstract關鍵字修飾的類,或者類沒有被聲明為abstract,但是在類中存在抽象成員的類。對于這兩種情況,PHP內核作了區分,類的結構體zend_class_entry.ce_flags中保存了這些信息,二者對應的值為ZEND_ACC_EXPLICIT_ABSTRACT_CLASS和ZEND_ACC_IMPLICIT_ABSTRACT_CLASS,這兩個值在前面的第一節已經做了介紹。
標記類為抽象類或標記成員方法為抽象方法的確認階段是語法解析階段。標記為抽象類與標記為接口等的過程一樣。而通過標記成員方法為抽象方法來確認一個類為抽象類則是在聲明函數時實現的。從第四章中我們知道編譯時聲明函數會調用zend_do_begin_function_declaration函數。在此函數中有如下代碼:
if (fn_flags & ZEND_ACC_ABSTRACT) {
CG(active_class_entry)->ce_flags |= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS;
}
}
若函數為抽象函數,則設置類的ce_flags為ZEND_ACC_IMPLICIT_ABSTRACT_CLASS,從而將這個類設置為抽象類。
抽象類,接口,普通類都是保存在zend_class_entry結構體中,他們只通過一個標志字段來區分,抽象類和接口還有一個共性:無法實例化。那我們看看Zend在那里限制的。要實例化一個對象我們只能使用new關鍵字來進行。下面是執行new是進行的操作:
static int ZEND_FASTCALL ZEND_NEW_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zval *object_zval;
zend_function *constructor;
?
if (EX_T(opline->op1.u.var).class_entry->ce_flags & (ZEND_ACC_INTERFACE|ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) {
char *class_type;
?
if (EX_T(opline->op1.u.var).class_entry->ce_flags & ZEND_ACC_INTERFACE) {
class_type = "interface";
} else {
class_type = "abstract class";
}
zend_error_noreturn(E_ERROR, "Cannot instantiate %s %s", class_type, EX_T(opline->op1.u.var).class_entry->name);
}
// ...
}
代碼很好理解,進行了簡單的判斷,如果為抽象類、隱式抽象類或者接口都無法進行實例化操作。
類的繼承、多態、封裝,以及訪問控制,接口,抽象類等都是基于類的結構實現的,因為這幾個類型只有個別的特性的差異,其他基本一致。如果要真正理解這些特性,需要更多的關注類的結構,基礎往往很重要,而在程序,數據結構就是程序的基礎。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊