無法將物件參考強制轉換為其靜態類型未聲明實作的介面,即使物件在執行期確實實作該介面。編譯器僅依靜態型別檢查。
物件參考轉介面參考時,編譯器會調整指標位址(指向介面虛函數表);介面轉回物件時,位址不會自動調回,導致後續存取非法位址。
type
I1 = interface
function DoIt: Boolean;
end;
TC1 = class
ATT1: Integer;
end;
TC2 = class(TC1, I1)
ATT2: Integer;
function DoIt: Boolean;
end;
var
Intf1: I1;
OBJ1: TC1;
OBJ2: TC2;
OBJ2 := TC2.Create;
OBJ1 := OBJ2; // 合法:子類指派給父類
I1(OBJ2).DoIt; // 合法:OBJ2 靜態類型聲明實作 I1
I1(OBJ1).DoIt; // 編譯失敗:TC1 未聲明實作 I1
// 危險的雙向轉換
OBJ2 := TC2.Create;
OBJ2.ATT1 := 0;
Intf1 := OBJ2; // 物件→介面:位址偏移
OBJ2 := Intf1; // 介面→物件:位址未偏回!
TC2(Intf1).ATT1 := 0; // 運行期非法位址存取
OBJ2.ATT1 := 0; // 同樣崩潰
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
Delphi 要求介面實作參考計數機制,否則無法正確管理物件存活。
介面變數設為 nil 時,編譯器自動呼叫 _IntfClear → _Release。若物件已手動 Free,_Release 存取已釋放記憶體導致 AV 錯誤:
OBJ2 := TC2.Create;
try
Intf1 := OBJ2;
Intf1.DoIt;
finally
OBJ2.Free;
Intf1 := nil; // 觸發 _Release → 存取已釋放物件 → AV
end;
編譯器「聰明地」在 try-finally 末尾自動插入 Intf1 := nil,同樣觸發 _Release。
Intf1 := nil;
OBJ2.Free;
Pointer(Intf1) := nil;
繞過 _IntfClear,直接歸零指標,不觸發參考計數,順序無關。
Free 時會將元素設為 nil → 觸發 _Release,不適用。InterList.Add(Pointer(I1(Obj)));釋放時無特殊處理。但必須確保轉介面後再轉指標,否則虛函數表位址錯誤。其 _Release 會在計數歸零時呼叫 Destroy,介面置 nil 可能意外銷毀物件,除非刻意設計共享所有權,否則不建議繼承,優先選擇 TComponent(_Release 非 COM 物件時回傳 -1,不執行銷毀)。
Delphi 介面機制源自 COM 模型,背負歷史包袱導致語言層面行為與純 OOP 直覺脫節。理解「介面指標≠物件指標」與「編譯器自動參考計數」是避坑關鍵。