使 .NET 類型適合 COM 互操作

將 .NET 類型公開至 COM

如果您想要將元件中的類型公開給 COM 應用程式,請在設計階段考慮 COM 相容性的需求。 當您遵守下列指導方針時,Managed 類型(類別、介面、結構和列舉)與 COM 類型緊密整合:

  • 類別應該明確地實作介面。

    雖然 COM Interop 提供一種機制來自動產生介面,其中包含類別的所有成員及其基類的成員,但最好提供明確的介面。 自動產生的介面稱為類別介面。 如需指導方針,請參閱 類別介面簡介。

    您可以使用 Visual Basic、C# 和 C++在程式代碼中納入介面定義,而不必使用介面定義語言 (IDL) 或其對等專案。 如需語法詳細數據,請參閱您的語言檔。

  • 被管理的類型必須是公共的。

    只有組件中的公用類型會被註冊並匯出到型別程式庫。 因此,COM 只能看到公用類型。

    受管理類型會公開某些功能給其他受管理的程式代碼,而這些功能可能並未公開給 COM。 例如,參數化建構函式、靜態方法和常數位段不會公開給 COM 用戶端。 此外,當執行階段封送和對型別的數據進行內外處理時,數據可能會被複製或轉換。

  • 方法、屬性、欄位和事件必須是公用的。

    要讓成員在 COM 中可見,公用類型的成員也必須是公用的。 您可以套用 ComVisibleAttribute來限制元件、公用類型或公用類型的公用成員的可見性。 根據預設,所有公用類型和成員都是可見的。

  • 類型必須有公用無參數建構函式,才能從 COM 啟動。

    受控的公用類型對 COM 是可見的。 不過,如果沒有公用無參數建構函式(不含自變數的建構函式),COM 用戶端就無法建立類型。 如果 COM 用戶端已被其他方式啟用,仍然可以使用該類型。

  • 類型不可以是抽象的。

    COM 用戶端和 .NET 用戶端都無法建立抽象類型。

匯出至 COM 時,受控類型的繼承階層會扁平化。 受控與非受控環境之間的版本控制也不同。 公開給 COM 的類型與其他 Managed 類型沒有相同的版本控制特性。

在 .NET 中使用 COM 類型

如果您想要從 .NET 取用 COM 類型,而且不想使用 Tlbimp.exe 之類的工具(類型庫匯入工具),您必須遵循下列指導方針:

  • 介面必須套用ComImportAttribute。
  • 介面必須套用介面 ID 至 COM 介面上的 GuidAttribute。
  • 介面應該已 InterfaceTypeAttribute 套用 ,以指定這個介面的基底介面類型 (IUnknown、 IDispatch或 IInspectable)。
    • 默認選項是具有的基底類型 IDispatch ,並將宣告的方法附加至介面的預期虛擬函式數據表。
    • 只有 .NET Framework 支援指定 的 IInspectable基底類型。

這些指導方針提供常見案例的最低需求。 還有更多自定義選項存在,而且會在 套用 Interop 屬性中說明。

在 .NET 中定義 COM 介面

當 .NET 程式代碼嘗試透過具有 ComImportAttribute 屬性的介面呼叫 COM 物件上的方法時,它必須建置虛擬函式數據表(也稱為 vtable 或 vftable),以形成介面的 .NET 定義,以判斷要呼叫的原生程序代碼。 此程序很複雜。 下列範例顯示一些簡單的案例。

考慮具有一些方法的 COM 介面:

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

針對此介面,下表描述其虛擬函式數據表配置:

IComInterface 虛擬函式表槽 方法名稱
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

每個方法都會依宣告的順序新增至虛擬函式數據表。 特定順序是由C++編譯程式所定義,但在沒有多載的簡單案例中,宣告順序會定義數據表中的順序。

宣告對應至此介面的 .NET 介面,如下所示:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid(/* The IID for IComInterface */)]
interface IComInterface
{
    void Method();
    void Method2();
}

InterfaceTypeAttribute指定基底介面。 它提供一些選項:

ComInterfaceType 值 基底介面類型 屬性化介面上成員的行為
InterfaceIsIUnknown IUnknown 虛擬函式表首先包含 IUnknown 的成員,然後再依宣告順序排列此介面的成員。
InterfaceIsIDispatch IDispatch 成員不會新增至虛擬函數表。 它們只能透過 IDispatch存取。
InterfaceIsDual IDispatch 虛擬函式表首先包含 IDispatch 的成員,然後再依宣告順序排列此介面的成員。
InterfaceIsIInspectable IInspectable 虛擬函式表首先包含 IInspectable 的成員,然後再依宣告順序排列此介面的成員。 僅支援 .NET Framework。

COM 介面繼承和 .NET

使用 ComImportAttribute 的 COM Interop 系統不會與介面繼承互動,因此除非採取某些緩和步驟,否則可能會導致非預期的行為。

使用 屬性的 System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute COM 來源產生器會與介面繼承互動,因此其行為會如預期般運作。

C++ 中的 COM 介面繼承

在C++中,開發人員可以宣告衍生自其他 COM 介面的 COM 介面,如下所示:

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

struct IComInterface2 : public IComInterface
{
    STDMETHOD(Method3)() = 0;
};

此宣告樣式經常用來做為將方法新增至 COM 對象的機制,而不需要變更現有的介面,這會是重大變更。 此繼承機制會產生下列虛擬函式數據表設定:

IComInterface 虛擬函式表槽 方法名稱
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 虛擬函式表槽 方法名稱
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

因此,從IComInterface很容易呼叫在IComInterface2*上定義的方法。 具體而言,在基底介面上呼叫方法不需要呼叫 QueryInterface ,即可取得基底介面的指標。 此外,C++允許從IComInterface2*隱式轉換成IComInterface*,且此轉換已妥善定義,讓您避免再次呼叫QueryInterface。 因此,在 C 或 C++ 中,如果您不想要,您就不需要呼叫 QueryInterface 來取得基底類型,這可以允許一些效能改善。

備註

WinRT 介面不會遵循此繼承模型。 它們被定義為遵循在 .NET 中基於 [ComImport] 的 COM 互操作模型相同的模型。

介面繼承與 ComImportAttribute

在 .NET 中,看起來像介面繼承的 C# 程式代碼實際上不是介面繼承。 請考慮下列程式碼:

interface I
{
    void Method1();
}
interface J : I
{
    void Method2();
}

此程式碼並未表示「J 實作 I」。程式碼實際上表示「任何實作 J 的類型也必須實作 I」。這種差異導致基本設計決定,使 ComImportAttribute-基於的 interop 中的介面繼承變得不便利。 介面始終會獨立考量;介面的基底介面清單不會影響任何計算,以決定指定 .NET 介面的虛擬函式表格。

因此,先前 C++ COM 介面範例的自然等同會導致不同的虛擬函式表配置。

C# 程式代碼:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

虛擬函式表佈局

IComInterface 虛擬函式表槽 方法名稱
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 虛擬函式表槽 方法名稱
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

由於這些虛擬函式表與 C++ 範例不同,執行時會造成嚴重問題。 .NET ComImportAttribute 中這些介面的正確定義如下:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    new void Method();
    new void Method2();
    void Method3();
}

在元數據層級,IComInterface2 不實作 IComInterface,只是指定 IComInterface2 的實作者必須實作 IComInterface。 因此,必須重新宣告基底介面類型中的每個方法。

介面繼承與 GeneratedComInterfaceAttribute (.NET 8 和更新版本)

COM 來源產生器由 GeneratedComInterfaceAttribute 觸發時,會將 C# 介面繼承實作為 COM 介面繼承,因此虛擬函式表會如預期般配置。 如果您採用上述範例,.NET System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute 中這些介面的正確定義如下所示:

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

基底介面的方法不需要重新宣告,也不應該重新宣告。 下表描述產生的虛擬函式資料表:

IComInterface 虛擬函式表槽 方法名稱
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 虛擬函式表槽 方法名稱
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

如您所見,這些數據表符合C++範例,因此這些介面會正常運作。

另請參閱