建置 COM 元件的互通
如果您計劃將來要撰寫以 COM 為主的應用程式,可以將您的程式碼設計成與 Managed 程式碼有效地進行互通。 包含進階計劃在內,您也可以簡化 Unmanaged 程式碼和 Managed 程式碼之間的轉換。
下列建議概述了撰寫與 Managed 程式碼互動的 COM 型別的最佳作法。
提供型別程式庫
在大多數的情況下,Common Language Runtime 需要所有型別的中繼資料,包括 COM 型別。 包含在 Windows Software Development Kit (SDK) 中的型別程式庫匯入工具 (Tlbimp.exe) 可以將 COM 型別程式庫轉換為 .NET Framework 中繼資料。 一旦將型別程式庫轉換為中繼資料,Managed 用戶端就可以無縫地呼叫 COM 型別。 針對簡易操作,永遠提供型別程式庫中的型別資訊。
您可以將型別程式庫封裝成個別的檔案,或將它當成資源嵌入 .dll、.exe 或 .ocx 檔案內。 另外,您可以直接產生中繼資料,允許您用發行者的金鑰組 (Key Pair) 簽名中繼資料。 以金鑰簽名的中繼資料具有限定的來源,當呼叫端擁有不正確的金鑰時可助於避免繫結,從而增強安全性。
註冊型別程式庫
若要正確地封送處理呼叫,Runtime 可能需要找出描述特定型別的型別程式庫。 除了在晚期繫結 (Late Binding) 的狀況下之外,型別程式庫必須在 Runtime 可以看到它之前先被註冊。
您可以利用將 regkind 旗標設為 REGKIND_REGISTER 來呼叫 Win32 API LoadTypeLibEx 函式,以註冊型別程式庫。 Regsvr32.exe 會自動註冊內嵌於 .dll 檔案內的型別程式庫。
使用安全陣列代替可變長度陣列
COM 安全陣列是自我描述的陣列。 檢查安全陣列,執行階段封送處理器可以判斷陣序、大小、範圍和通常在執行階段陣列內容的型別。 可變長度 (或 C-Style) 陣列不需要相同的自我描述品質。 例如,下列 Unmanaged 方法簽章 (Signature) 不提供除了項目型別之外陣列參數的相關資訊。
HRESULT DoSomething(int cb, [in] byte buf[]);
事實上,陣列無法辨別任何其他以傳址 (By Reference) 方式傳遞的參數。 因此,Tlbimp.exe 不會轉換 DoSomething 方法的陣列參數。 反而是陣列會顯示為 Byte 型別的參考,如下列程式碼所示。
Public Sub DoSomething(cb As Integer, ByRef buf As Byte)
public void DoSomething(int cb, ref Byte buf);
若要增強互通,您可以在 Unmanaged 方法簽章中輸入引數做為 SAFEARRAY。 例如:
HRESULT DoSomething(SAFEARRAY(byte)buf);
Tlbimp.exe 會將 SAFEARRAY 轉換成為以下 Managed 陣列型別:
Public Sub DoSomething(buf As Byte())
public void DoSomething(Byte[] buf);
使用 Automation 相容的資料型別
Runtime 封送處理服務自動支援所有的 Automation 相容的資料型別。 可能支援或不支援不相容的型別。
提供型別程式庫中的版本和地區設定
當您匯入型別程式庫時,也會將型別程式庫版本和地區設定資訊傳播到組件中。 於是,Managed 用戶端就可以繫結到組件的特定版本或地區設定,或組件的最新版本中。 提供型別程式庫中的版本資訊可以讓用戶端精確地選擇要使用哪一個組件的版本。
使用 Blittable 型別
資料型別不是 Blittable 就是非 Blittable。 Blittable 型別具有跨越 Interop 界限的共同表示。 整數和浮點數型別是 Blittable。 Blittable 型別的陣列和結構也是 Blittable。 字串、日期和物件為非 Blittable 型別的範例,它們會在封送處理過程中被轉換。
Interop 封送處理服務支援 Blittable 和非 Blittable 型別;不過,在封送處理期間需要轉換的型別以及 Blittable 型別並不會執行轉換。 當您使用非 Blittable 型別時,請注意有一項與封送處理非 Blittable 型別關聯的經常花費。
尤其是字串是不確定的。 Managed 字串會被儲存成 Unicode 字元,因此可以更有效地封送處理到 Unmanaged 程式碼,其預期 Unicode 字元引數。 可能的話,最好避免 ANSI 字元組成的字串。
實作 IProvideClassInfo
當封送處理 Unmanaged 介面到 Managed 程式碼時,執行階段會建立特定型別的包裝函式。 方法簽章通常指示介面的型別,但實作介面的物件型別可能未知。 如果物件的型別未知,執行階段會以泛用的 COM 物件包裝函式來包裝介面,這個包裝函式的作用比型別特定的包裝函式還要少。
例如,請考慮以下的 COM 方法簽章:
interface INeedSomethng {
HRESULT DoSomething(IBiz *pibiz);
}
當匯入時方法會被轉換成如下:
Interface INeedSomething
Sub DoSomething(pibiz As IBiz)
End Interface
interface INeedSomething {
void DoSomething(IBiz pibiz);
}
如果您將實作 INeedSomething 介面的 Managed 物件傳遞到 IBiz 介面,Interop 封送處理器會在 IBiz 初始引入至 Managed 程式碼上嘗試使用特定型別的物件包裝函式來包裝介面。 若要識別包裝函式的正確型別,封送處理器必須知道實作介面的物件型別。 封送處理器試圖決定物件型別的其中一個方式是查詢 IProvideClassInfo 介面。 如果物件實作 IProvideClassInfo,封送處理器會決定物件的型別,並且在輸入的包裝函式中包裝介面。
使用模組呼叫
在 Managed 與 Unmanaged 程式碼之間封送處理資料會有成本負擔。 您可以減少跨界限的轉換來降低成本負擔。 一般來說,可最少化轉換次數的模組介面執行效果比經常跨界限、每一次跨越時執行少量工作的介面還要好。
謹慎使用失敗的 HRESULT
當 Managed 用戶端呼叫 COM 物件時,Runtime 會將 COM 物件的失敗 HRESULT 對應到封送處理器從呼叫返回時所擲回的例外狀況中。 Managed 例外狀況模型已經為非例外的案例進行最佳化;當沒有發生例外狀況時,幾乎不會有與攔截例外狀況關聯的負荷。 相反地,當發生例外狀況時,攔截例外狀況的代價可能會非常高。
基於資訊因素,請謹慎使用例外狀況,並避免傳回失敗的 HRESULT。 為例外的狀況保留失敗的 HRESULT。 不過,過度使用失敗的 HRESULT 可能會影響效能。
明確釋放外部資源
某些物件會在它們的存留期 (Lifetime) 期間使用外部資源;例如,資料庫連接可能更新資料錄集 (Recordset)。 一般來說,物件會在其存留期期間保持外部資源,但明確釋放卻可以立即傳回資源。 例如,您可以使用檔案物件上的 Close 方法,而非在類別解構函式中關閉檔案,或使用 IUnknown.Release。 在程式碼中提供 Close 方法的對等用法,即使檔案物件依舊存在,您仍可以釋放外部檔案資源。
避免重複定義 Unmanaged 型別
在 Managed 程式碼中實作現有 COM 介面的正確方式,是由使用 Tlbimp.exe 或相同 API 匯入介面的定義開始。 產生的中繼資料提供 COM 介面的相容定義 (相同的 IID 或相同的 DispId 等等)。
避免以手動方式在 Managed 程式碼中重複定義 COM 介面。 這個動作會消耗時間,而且很少會產生與現有 COM 介面相容的 Managed 介面。 請使用 Tlbimp.exe 維護定義相容性來代替上述動作。
避免使用成功的 HRESULT
攔截例外狀況是 Managed 應用程式處理錯誤情形最自然的方式。 若要利用 COM 型別透明性,每當 COM 方法傳回失敗的 HRESULT 時,執行階段就會自動擲回例外狀況。
如果您的 COM 物件傳回成功的 HRESULT,執行階段會傳回在 retval 參數中的任何值。 根據預設值,HRESULT 會被捨棄,而使得 Managed 用戶端很難檢查成功 HRESULT 的值。 雖然您可以利用 PreserveSigAttribute 屬性保留 HRESULT,但是這個過程相當費力。 您必須手動將屬性加入由 Tlbimp.exe 或相同 API 所產生的組件中。
最好是盡可能避免成功的 HRESULT。 不過,您可以透過 Out 參數傳回有關呼叫狀態的資訊。
避免使用模組函式
型別程式庫可以包含模組上定義的函式。 通常,您會使用這些函式來提供型別資訊給 DLL 進入點 (Entry Point)。 Tlbimp.exe 不會將這些函式匯入。
避免在預設的介面中使用 System.Object 的成員
Managed 用戶端及 COM Coclass 可以和執行階段所提供的包裝函式說明互動。 當您匯入 COM 型別時,轉換過程會將 Coclass 預設介面上的所有方法加入衍生自 System.Object 類別的包裝函式類別。 命名預設介面的成員時請小心,以避免與 System.Object 成員產生命名衝突。 如果發生衝突時,匯入的方法會覆寫基底類別方法。
如果預設介面的方法和 System.Object 的方法提供相同的功能,則這個動作可能會有所幫助。 但是,如果以非計劃的方式使用預設介面的方法,可能會發生問題。 若要防止命名衝突,請避免在預設介面中使用以下名稱:Object、Equals、Finalize、GetHashCode、GetType、MemberwiseClone 和 ToString。