在 Windows COM 程式設計中,Variant 與 IDispatch 是支撐腳本語言、動態綁定與跨語言互操作的兩大基石。本文將從結構定義、設計動機、實現機制三個維度,深度剖析這兩大核心技術,並釐清「雙介面」的真實含義。
C++ 是強類型語言,變量類型在編譯期確定。但 Visual Basic、JavaScript 等腳本語言採用弱類型或動態類型,變量可在運行時改變類型。為了讓 C++ 組件能與這些語言交換數據,COM 引入了 VARIANT —— 一個能存放「幾乎所有類型」數據的結構體。
VARIANT 本質是一個巨大的 union,包含 VARTYPE vt 標識當前實際類型,以及各類數據字段:
struct tagVARIANT {
VARTYPE vt; // 類型標譤
WORD wReserved1, wReserved2, wReserved3;
union {
LONG lVal; // 32 位整數
DOUBLE dblVal; // 雙精度浮點
BSTR bstrVal; // 字符串
IDispatch *pdispVal; // IDispatch 指針
SAFEARRAY *parray; // 安全數組
// ... 大量其他類型與指針變體
struct { PVOID pvRecord; IRecordInfo *pRecInfo; } __VARIANT_NAME_4; // 用戶定義類型
};
};
這就是為什麼 C++ 程式員覺得它「超級複雜」——它其實是一個手工實現的「類型安全聯合體」。
VariantInit、VariantClear、VariantChangeType 等 APIBSTR、SAFEARRAY 未正確釋放)相比之下,Delphi、C# 等語言提供內置 Variant 支持,大幅降低使用門檻。
腳本語言(解釋執行)無法像 C++ 那樣在編譯期綁定虛函數表地址。它們需要:在完全不知組件介面定義的情況下,通過名稱或 ID 動態調用方法/屬性。這就是「晚綁定」。
IDispatch 正是為此而生的標準化規範,核心方法:
GetIDsOfNames:將方法名(字符串)映射為 DISPIDInvoke:根據 DISPID、參數包(DISPPARAMS,內含 VARIANT 數組)執行調用,返回 VARIANT 結果這與 Windows 消息機制(uMsg + wParam/lParam)極其相似:DISPID 對應消息 ID,DISPPARAMS 對應參數結構。
| 維度 | 早綁定 | 晚綁定 |
|---|---|---|
| 典型語言 | C++、Java | VB、JavaScript、VBScript |
| 綁定時機 | 編譯/鏈接期 | 運行期 |
| 調用機制 | 虛函數表 | IDispatch::Invoke |
| 性能 | 極高 | 較低(名稱查找、參數封裝/解封) |
| 靈活性 | 需預知介面 | 完全動態 |
許多人誤以為「繼承自 IDispatch 的介面就是雙介面」。實際上:
雙介面 = 同時實現
IDispatch(晚綁定)與 vtable 介面(早綁定)
[dual]
interface IFoo : IDispatch {
HRESULT foo(int arg1, int arg2);
};
[dual] 屬性指示 MIDL 編譯器生成雙重調用機制fooIDispatch::Invoke 動態調用同一方法IFoo 必須繼承自 IDispatch,但繼承 IDispatch 的介面不一定是雙介面GetTypeInfo 提供完整元數據,支援智能提示、自動完成VARIANT 是跨語言數據交換的「通用貨幣」,代價是複雜度與運行期風險IDispatch 實現了「不知介面仍能調用」的晚綁定機制,核心是 DISPID + Invoke[dual])是 COM 為兼顧效率與靈活性而設計的混合模式,是 Automation 組件的標準選擇理解這三者關係,是掌握 COM/Automation 開發的關鍵一環。