Delphi 介面編程的「Hello World」不是單介面,而是一個類實現多個介面,展示 QueryInterface 如何根據介面類型返回不同能力視圖——這正是 COM/介面設計的核心價值。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
// 介面 1:計算能力
ICalculator = interface
['{C3D4E5F6-A7B8-9012-CDEF-345678901234}']
function Add(a, b: Integer): Integer;
function Mul(a, b: Integer): Integer;
end;
// 介面 2:顯示能力
IDisplay = interface
['{D4E5F6A7-B8C9-0123-DEFA-456789012345}']
procedure ShowMsg(const Msg: string);
procedure ShowValue(const Caption: string; Value: Integer);
end;
// 單一類實現兩個介面
TMyTool = class(TInterfacedObject, ICalculator, IDisplay)
public
function Add(a, b: Integer): Integer;
function Mul(a, b: Integer): Integer;
procedure ShowMsg(const Msg: string);
procedure ShowValue(const Caption: string; Value: Integer);
end;
TForm1 = class(TForm)
btnObj: TButton; // 直接用對象調用
btnCalc: TButton; // 用 ICalculator
btnDisp: TButton; // 用 IDisplay
btnBoth: TButton; // 雙介面切換
procedure btnObjClick(Sender: TObject);
procedure btnCalcClick(Sender: TObject);
procedure btnDispClick(Sender: TObject);
procedure btnBothClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TMyTool }
function TMyTool.Add(a, b: Integer): Integer;
begin
ShowMessage('ICalculator.Add');
Result := a + b;
end;
function TMyTool.Mul(a, b: Integer): Integer;
begin
ShowMessage('ICalculator.Mul');
Result := a * b;
end;
procedure TMyTool.ShowMsg(const Msg: string);
begin
ShowMessage('IDisplay.ShowMsg: ' + Msg);
end;
procedure TMyTool.ShowValue(const Caption: string; Value: Integer);
begin
ShowMessage(Caption + ': ' + IntToStr(Value));
end;
{ TForm1 }
procedure TForm1.btnObjClick(Sender: TObject);
var
Obj: TMyTool;
begin
Obj := TMyTool.Create;
try
Obj.Add(3, 5);
Obj.Mul(3, 5);
Obj.ShowMsg('對象直接調用');
Obj.ShowValue('結果', 42);
finally
Obj.Free; // 對象變量必須手動 Free
end;
end;
procedure TForm1.btnCalcClick(Sender: TObject);
var
Calc: ICalculator;
begin
Calc := TMyTool.Create; // 介面變量,自動引用計數
ShowMessage('3+5=' + IntToStr(Calc.Add(3, 5)));
ShowMessage('3*5=' + IntToStr(Calc.Mul(3, 5)));
// Calc 只能訪問 ICalculator 方法,IDisplay 不可見
// 過程結束自動釋放
end;
procedure TForm1.btnDispClick(Sender: TObject);
var
Disp: IDisplay;
begin
Disp := TMyTool.Create;
Disp.ShowMsg('僅顯示介面');
Disp.ShowValue('答案', 42);
// Disp 只能訪問 IDisplay 方法
end;
procedure TForm1.btnBothClick(Sender: TObject);
var
Obj: TMyTool;
Calc: ICalculator;
Disp: IDisplay;
begin
Obj := TMyTool.Create; // 持有對象引用 (RefCount=1)
try
Calc := Obj; // 隱式 QueryInterface → ICalculator (RefCount=2)
Disp := Obj; // 隱式 QueryInterface → IDisplay (RefCount=3)
Calc.Add(10, 20);
Disp.ShowValue('和', 30);
// 關鍵:介面賦值之間可相互轉換 (QueryInterface)
Disp := Calc; // ICalculator → IDisplay (同一對象不同視圖)
Disp.ShowMsg('從 Calc 轉來的 Disp');
finally
Obj.Free; // 手動釋放對象,介面變量離開作用域再 _Release 兩次
end;
end;
end.
TMyTool 實例 (RefCount=3)
│
├─ 作為對象變量 Obj ─────────────────────→ 可調用所有 public 方法
│
├─ QueryInterface(ICalculator) → Calc ───→ 只見 Add, Mul
│
└─ QueryInterface(IDisplay) → Disp ───→ 只見 ShowMsg, ShowValue
關鍵:Calc := Disp 之間轉換,本質是同一對象內部 QueryInterface,
返回同一對象的不同介面指針,RefCount 相應增減。
| # | 規則 | 說明 |
|---|---|---|
| 1 | 命名慣例 | 介面以 I 開頭 (如 ICalculator),類以 T 開頭 |
| 2 | 根介面 | 所有介面隱式繼承 IInterface (即 IUnknown),可省略不寫 |
| 3 | 成員限制 | 只能有方法、屬性,**無字段** |
| 4 | 可見性 | 介面成員**隱式 public**,無 private/protected/public 修飾符 |
| 5 | 無虛擬修飾 | 介面方法無 virtual/dynamic/abstract/override,純契約 |
| 6 | 介面繼承 | 介面可單繼承介面 (IChild = interface(IParent)),Win32 不支援多繼承 |
| 7 | 類實現多介面 | TClass = class(TBase, IIntf1, IIntf2, ...) 逗號分隔 |
| 8 | 介面視圖隔離 | 介面變量**只見自身聲明的成員**,類多豐富介面只暴露契約 |
| 9 | 基類選擇 | 實現介面類**強烈建議繼承 TInterfacedObject**,自帶引用計數;繼承 TObject 需手動實現 _AddRef/_Release |
| 10 | 自動釋放 | 介面變量離開作用域/賦 nil/重新賦值 → 自動 _Release → 歸零調用 Destroy;**對象變量無此機制** |
as 轉換的特殊說明
// 介面有 GUID 才能用 as
Disp := Calc as IDisplay; // 安全轉換,失敗拋 EIntfCastError
// 無 GUID 介面只能用硬轉換 (不安全,失敗 AV)
Disp := IDisplay(Calc); // 編譯器信任你,運行時無檢查
// 或
Disp := Calc as IDisplay; // 編譯錯誤:介面無 GUID
建議:所有跨單元/跨模塊介面**統一加 GUID** (Ctrl+Shift+G),內部單元可省略但建議養成習慣。
這個「第一個例子」其實展示了介面編程的三大核心能力:
class(..., IIntf1, IIntf2) 實現多契約。ICalculator 變量只見計算方法,IDisplay 只見顯示方法——接口隔離原則 (ISP) 天然落地。Calc := Disp 運行時查詢同一對象的不同能力視圖,無需知道具體類型。掌握這三點,你就掌握了 Delphi 介面編程 80% 的實戰精髓。