介面是 Delphi 實現「契約導向設計」、「多型解耦」、「COM 互操作」的核心機制。本文系統梳理介面宣告、實現、獲取、生命週期管理、委派模式與 RTTI 反射,並對比類別繼承差異,附完整代碼範例。
「電影院、歌劇院、客運站、經理人都能賣票,但不適合共用繼承體系。定義
ITicketSeller介面,誰實現誰就能賣票,調用者只依賴介面不依賴具體類別。」
type
IMyInterface = interface(IInterface) // 或 IUnknown
['{63E072DF-B81E-4734-B3CB-3C23C7FDA8EA}'] // GUID,COM 互操作必須
function GetName(const S: String): String; stdcall;
// IInterface 三大核心方法 (若繼承 IInterface 編譯器自動實現)
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
Ctrl+Shift+G 產生。有 GUID 才能用 Supports、as 轉型、COM 封送。
type
TIntfClass = class(TInterfacedObject, IMyInterface) // Delphi 7+ 建議繼承 TInterfacedObject
private
FCounter: Integer;
public
function GetName(const S: String): String; stdcall;
// QueryInterface/_AddRef/_Release 由 TInterfacedObject 實現
end;
function TIntfClass.GetName(const S: String): String;
begin
Result := 'Hello ' + S + ' from ' + ClassName;
end;
| 方式 | 代碼 | 條件 |
|---|---|---|
| 硬轉型 | Intf := IMyInterface(Obj) | Obj 必須實現該介面,否則編譯錯誤 |
| 隱含轉型 | Intf := Obj | 編譯器自動生成 QueryInterface 呼叫 |
| QueryInterface | OleCheck(Obj.QueryInterface(IID_IMyInterface, Intf)) | 需 GUID,僅限 COM 介面 |
| as 操作符 | Intf := Obj as IMyInterface | 需繼承 IInterface 且有 GUID,失敗拋 EInvalidCast |
var Obj: TIntfClass; Intf: IMyInterface;
begin
Obj := TIntfClass.Create;
try
Intf := Obj; // _AddRef → RefCount=1
Intf.GetName('A');
finally
Intf := nil; // _Release → RefCount=0 → Obj 自動 Free
FreeAndNil(Obj); // 雙重 Free → AV 存取違規!
end;
end;
解決方案:在 _Release 中僅遞減計數,不釋放物件,由外部統一管理物件壽命:
function TIntfClass._Release: Integer;
begin
Result := InterlockedDecrement(FRefCount);
// 不要在這裡呼叫 Destroy/Free
end;
或改用 TInterfacedObject 預設行為:介面釋放即釋放物件,不要 再手動 Free 物件。
// 既有實現
type
IImpl = interface(IInterface)
['{...}']
function ConvertToUSD(const NTD: Integer): Double; stdcall;
end;
TImpl = class(TInterfacedObject, IImpl)
function ConvertToUSD(const NTD: Integer): Double; stdcall;
end;
// 新服務類別委派給 TImpl
type
TService = class(TInterfacedObject, IImpl)
private
FImpl: IImpl;
public
constructor Create; overload;
constructor Create(AClass: TClass); overload;
destructor Destroy; override;
property Impl: IImpl read FImpl implements IImpl; // 關鍵字 implements
end;
constructor TService.Create;
begin
FImpl := TImpl.Create;
end;
constructor TService.Create(AClass: TClass);
var Inst: TImpl;
begin
Inst := TImpl(AClass.NewInstance);
FImpl := Inst.Create;
end;
destructor TService.Destroy;
begin
FImpl := nil; // 釋放介面引用
inherited;
end;
implements 關鍵字讓編譯器自動生成介面方法轉發代碼。Delphi 在 VMT 偏移 -72 (vmtIntfTable) 存放介面表。啟用 {$M+} 產生介面 RTTI。
{$M+}
IInvokable = interface(IInterface)
{$M-}
type
TInterfaceEntry = packed record
IID: TGUID; // 介面 GUID
VTable: Pointer; // 虛函數表指標
IOffset: Integer; // 實現類別偏移
ImplGetter: Integer; // 獲取實現器編碼
end;
TInterfaceTable = packed record
EntryCount: Integer;
Entries: array[0..9999] of TInterfaceEntry;
end;
GetInterfaceCount; // 介面數量
GetInterfaceTable; // 介面表指標
// 範例:列印類別實現的所有介面 GUID
var Table: PInterfaceTable; I: Integer;
begin
Table := PPointer(Integer(PPointer(Self)^) + vmtIntfTable)^;
for I := 0 to Table.EntryCount - 1 do
Memo1.Lines.Add(GUIDToString(Table.Entries[I].IID));
end;
TIntfMethEntryArray 包含方法名、呼叫慣例、參數個數、回傳類型、參數旗標/名稱/類型等完整元資料,支援動態呼叫、序列化、Web Service 發布等進階場景。
| 特性 | 介面 | 類別繼承 |
|---|---|---|
| 多重實現 | 單類別可實現多介面 | 單繼承 |
| 契約與實現分離 | 徹底 | 耦合 |
| 跨語言/進程 | 支援 (COM) | 不支援 |
| 引用計數管理 | 自動 | 手動 |
| 預設方法實現 | 無 (Delphi 10.4+ 支援 default) | 有 |
| 適用場景 | 插件架構、服務契約、COM、解耦 | IS-A 關係、共享實現 |
善用介面,能讓 Delphi 程式架構更靈活、可測試、可擴展,是邁向現代軟體工程的關鍵一步。