perlclassguts - feature 'class'
和類別語法的內部運作
此文件提供有關 perl 解譯器如何實作 feature 'class'
語法和整體行為的深入資訊。它並非作為如何使用該功能的最終使用者指南。有關這方面的資訊,請參閱 perlclass。
假設讀者大致熟悉 perl 解譯器的整體內部結構。有關這些詳細資訊的更一般概述,另請參閱 perlguts。
類別基本上是一個套件,並以與非類別套件完全相同的方式存在於符號表中,作為具有輔助結構的 HV。它與非類別套件的不同之處在於 HvSTASH_IS_CLASS()
巨集會對它傳回 true。
與其為類別相關的額外資訊儲存在附加到儲藏區的 struct xpvhv_aux
結構中,在下列欄位中
HV *xhv_class_superclass;
CV *xhv_class_initfields_cv;
AV *xhv_class_adjust_blocks;
PADNAMELIST *xhv_class_fields;
PADOFFSET xhv_class_next_fieldix;
HV *xhv_class_param_map;
xhv_class_superclass
對於沒有超類別的類別將會是 NULL
。如果已使用 :isa()
類別屬性設定了父類別,它將直接指向父類別的儲藏區。
xhv_class_initfields_cv
將包含一個 CV *
,指向一個函式,作為此類別或其任何子類別的建構函式的一部分來呼叫。此 CV 負責初始化此類別定義的所有欄位,以供新的執行個體使用。此 CV 將會是一個匿名的實體函式 - 亦即,雖然它沒有名稱和 GV,但它不是一個 proto 子例程,且可以被直接呼叫。
xhv_class_adjust_blocks
可能指向一個包含 CV 指標的 AV,指向類別上定義的每個 ADJUST
區塊。如果類別有一個超類別,這個陣列將會額外包含其父類別的 CV 的重複指標。AV 會在第一次將元素推入其中時延遲建立;沒有 AV 是有效的,而且在此情況下這個指標將會是 NULL
。
CV 會直接儲存,而不是透過 RV。每個 CV 都會是一個匿名的實體函式。
xhv_class_fields
會指向一個包含 PADNAME
的 PADNAMELIST
,每個 PADNAME
都是類別中定義的一個欄位。它們會依據宣告順序儲存。不過,請注意,這個陣列的索引不一定會等於每個欄位的 fieldix
,因為在子類別的情況下,陣列會從零開始,但如果其父類別包含任何欄位,則陣列中第一個欄位的索引將會是非零值。
如需瞭解如何表示個別欄位的更多資訊,請參閱 "欄位"。
xhv_class_next_fieldix
會提供將會指定給要新增到類別中的下一個欄位的欄位索引。它僅在編譯時有用。
xhv_class_param_map
可能會指向一個 HV,它會將欄位 :param
屬性名稱對應到具有該名稱的欄位的欄位索引。這個對應會從父類別複製;每個類別都會包含其所有父類別的總和,以及它自己的總和。
欄位在根本上仍然是在範圍中宣告的詞彙變數,而且存在於其對應 CV 的 PADNAMELIST
中。方法和其他類似方法的 CV 仍然可以完全擷取它們,就像它們可以對一般詞彙變數執行相同的動作一樣。欄位與其他類型的 pad 項目不同之處在於 PadnameIsFIELD()
巨集會對它傳回 true。
與其作為欄位相關的額外資訊會儲存在一個額外的結構中,可透過 padname 上的 PadnameFIELDINFO()
巨集存取。這個結構具有下列欄位
PADOFFSET fieldix;
HV *fieldstash;
OP *defop;
SV *paramname;
bool def_if_undef;
bool def_if_false;
fieldix
儲存欄位的「欄位索引」,也就是儲存此欄位值的實例欄位陣列索引。請注意,陣列中的第一個索引並未特別保留。類別中的第一個欄位將從欄位索引 0 開始。
fieldstash
儲存定義此欄位的類別快取指標。如果在同一個範圍內定義了多個類別,這將是必要的;它用於消除各個欄位的歧義。
{
class C1; field $x;
class C2; field $x;
}
defop
可能儲存此欄位的預設表達式 optree 指標。預設表達式是可選的;此欄位可能是 NULL
。
paramname
可能指向包含提供給欄位的 :param
名稱屬性的常規字串 SV。如果沒有,它將會是 NULL
。
如果預設表達式分別使用 //=
或 ||=
算子設定,則 def_if_undef
和 def_if_false
之一將為 true。
方法從本質上來說仍然是 CV,並且具有與 CV 相同的基本表示方式。它有一個 optree 和一個 pad,並透過其所包含套件的快取中的 GV 儲存。它與非方法 CV 的區別在於 CvIsMETHOD()
巨集將對其傳回 true。
(注意:此巨集不應與先前稱為 CvMETHOD()
的巨集混淆。該巨集與類別系統無關,並已重新命名為 CvNOWARN_AMBIGUOUS()
以避免此混淆。)
目前沒有需要儲存關於方法 CV 的額外資訊,因此結構不會新增任何新欄位。
物件實例由一個全新的 SV 類型表示,其基本類型為 SVt_PVOBJ
。這仍應祝福進入其類別快取,並以通常的方式包裝在 RV 中以供傳統物件使用。
由於這些是其自己的唯一容器類型,不同於雜湊或陣列,因此核心 builtin::reftype
函式在被詢問這些時會傳回一個新值。該值為 "OBJECT"
。
在內部,此類物件是一個 SV 指標陣列,其大小在建立時固定(因為類別中的欄位數在編譯後已知)。物件實例會儲存其內部的最大欄位索引(用於存取時的基礎錯誤檢查),以及儲存個別欄位值的固定大小 SV 指標陣列。
陣列和雜湊類型的欄位會直接將 AV 或 HV 指標儲存到陣列中;它們不會透過介入 RV 來儲存。
上述資料結構由下列 API 函式支援。
void class_setup_stash(HV *stash);
當解析器遇到 class
關鍵字時呼叫。它將儲存升級為類別,並準備接收類別特定項目,例如方法和欄位。
void class_seal_stash(HV *stash);
在 class
區塊結束時或對於單元類別的包含範圍由解析器呼叫。此函數執行各種最後完成活動,在類別實例建構之前需要執行,但必須等到已知類別成員的所有資訊後才能執行。
必須在這兩個函數呼叫之間新增或修改正在編譯的類別。一旦類別已封裝,就無法修改。
void class_add_field(HV *stash, PADNAME *pn);
由 pad.c 呼叫,作為在目前儲存區中定義新欄位名稱的一部分。請注意,此函數不會建立儲存區名稱;這必須已由 pad.c 完成。此 API 函數僅通知類別已建立新欄位名稱,現在可供其使用。
void class_add_ADJUST(HV *stash, CV *cv);
當解析器已解析和建構新 ADJUST
區塊的 CV 後呼叫。這會新增到類別儲存的清單中。
void class_prepare_initfield_parse();
在解析欄位變數的初始化運算式之前,由解析器呼叫。這會使用暫停的 compcv 將所有欄位初始化運算式組合到同一個 CV 中。
void class_set_field_defop(PADNAME *pn, OPCODE defmode, OP *defop);
在解析器解析完欄位的初始化表達式後呼叫。設定預設表達式和應用模式。defmode
應為零,或根據預設模式為 OP_ORASSIGN
或 OP_DORASSIGN
之一。
#define padadd_FIELD
此旗標常數告訴 pad_add_name_*
函數系列,新的名稱應新增為欄位。不需要呼叫 class_add_field()
;這將自動完成。
void class_prepare_method_parse(CV *cv);
在 start_subparse()
之後,但在執行任何其他操作之前,由解析器呼叫。這會準備 PL_compcv
以解析方法;安排 CvIsMETHOD
測試為 true,新增 $self
詞彙,以及可能需要的任何其他活動。
OP *class_wrap_method_body(OP *o);
在將方法主體解析為 optree 的最後,但在將其包裝在最終 CV 中之前,由解析器呼叫。此函數會在 optree 中插入額外的 ops,以使方法正常運作。
#define SVt_PVOBJ
與 SvTYPE()
巨集比較時使用的 SV 類型常數。
SSize_t ObjectMAXFIELD(sv);
一個函式型巨集,取得可從 ObjectFIELDS
陣列存取的最大有效欄位索引。
SV **ObjectFIELDS(sv);
一個函式型巨集,直接從物件執行個體取得欄位陣列。欄位可透過其欄位索引存取,從 0 到 ObjectMAXFIELD
給定的最大有效索引。
newUNOP_AUX(OP_METHSTART, ...);
一個 OP_METHSTART
是 UNOP_AUX
,必須存在於方法 CV 的開頭,才能讓它正常運作。這是由 class_wrap_method_body()
插入的,甚至出現在與簽章引數檢查或萃取相關的任何 optree 片段之前。
此 op 負責將 $self
的值從引數清單中移出,並將方法需要存取的任何欄位變數繫結到 pad 中。AUX 向量將包含所需欄位/pad 索引配對的詳細資料。
此 op 也會對呼叫者值執行健全性檢查。它會檢查它是否絕對是相容類別型態的物件參考。如果不是,就會擲回例外狀況。
如果 op_private
欄位包含 OPpINITFIELDS
旗標,這表示 op 開始特殊 xhv_class_initfields_cv
CV。在這種情況下,它應該另外從引數清單中取得第二個值,該值應該是純 HV 指標(直接,而非透過 RV),並將它繫結到第二個 pad 插槽,已產生 optree 會預期在那裡找到它。
OP_INITFIELD
僅在實例建構階段的 xhv_class_initfields_cv
CV 中作為一部分被呼叫。這是組成實例的可變動欄位 (包含 AV 和 HV) 的個別 SV 實際上被指定到 ObjectFIELDS
陣列的時間。OPpINITFIELD_AV
和 OPpINITFIELD_HV
私有旗標表示它是否正在建立 AV 或 HV;如果兩個都沒有設定,則建立 SV。
如果 op 具有 OPf_STACKED
旗標,它預期在堆疊中找到初始化值。對於 SV,這是資料堆疊中最上面的 SV。對於 AV 和 HV,它預期一個標記清單。
ADJUST
Phaser在編譯時間,ADJUST
phaser 的剖析以與現有的 perl phaser (BEGIN
等) 根本不同的方式處理。
剖析器不是採取通常的路線,而是辨識 ADJUST
關鍵字引入了 phaser 區塊。然後,剖析器以類似於剖析 (匿名的) 方法主體的方式剖析此區塊的主體,建立一個沒有名稱 GV 的 CV。然後,透過呼叫 class_add_ADJUST
將其直接插入類別資訊中,完全繞過符號表。
在編譯期間,類別和欄位的屬性會以不同於現有 perl 屬性在子常式和詞彙變數上的方式處理。
剖析器仍會形成一個由 OP_CONST
節點組成的 OP_LIST
optree,但這些節點會傳遞給 class_apply_attributes
或 class_apply_field_attributes
函式。剖析器並不會使用類別查詢來尋找正在剖析類別中的方法,而是使用已知屬性的內部固定清單來尋找函式,以將屬性套用至類別或欄位。未來可能會支援使用者提供的延伸屬性,但目前只會辨識核心本身定義的屬性。
在編譯期間,剖析器會在剖析欄位的預設表達式時使用暫停的 compcv。類別中所有欄位的表達式共用同一個暫停的 compcv,然後編譯成同一個內部 CV,由建構函式呼叫以初始化該類別提供的欄位。
類別本身產生的建構函數是一個 XSUB,它會依序執行三項工作:建立實例 SV 本身、呼叫欄位初始化程式,然後呼叫 ADJUST 區塊 CV。任何類別的建構函數永遠都是相同的基本形狀,不論類別是否有超類別。
欄位初始化程式會收集到一個稱為欄位初始化程式 CV 的已產生 optree 為基礎的 CV。這是包含欄位初始化表達式的所有 optree 片段的 CV。呼叫時,欄位初始化程式 CV 呼叫所有個別欄位初始化運算之前,可能會對超類別初始化程式(如果存在)進行連鎖呼叫。欄位初始化程式 CV 會在堆疊中呼叫兩個項目;實例 SV 和包含建構函數參數的直接 HV。請仔細注意:此 HV 是直接傳遞的,而不是透過 RV 參考。之所以允許這樣做,是因為呼叫者和被呼叫者都是直接產生的程式碼,而不是任意純 Perl 子常式。
ADJUST 區塊 CV 全部收集到一個單一的平面清單中,同時合併超類別定義的所有區塊。它們全部在欄位初始化程式 CV 之後依序呼叫。
$self
存取呼叫class_prepare_method_parse()
時,它會安排新 CV 主體的 pad 以稱為$self
的詞彙開頭。由於 pad 應在此時新建立,因此它的 pad 索引將為 1。函數會檢查這一點,如果不為真則會中止。
由於這個事實,方法或類似方法 CV 主體內的程式碼可以可靠地使用 pad 索引 1 來取得呼叫者參考。OP_INITFIELD
opcode 也依賴於這個事實。
類似地,在 xhv_class_initfields_cv
期間,下一個 pad 槽用於儲存建構函數參數 HV,在 pad 索引 2 中。
Paul Evans