限定 COM 交互操作的 .NET 類型

將 .NET 類型公開至 COM

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

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

    雖然 COM Interop 提供機制來自動產生包含類別之所有成員和其基底類別成員的介面,最好是目前提供明確的介面。但是最好是提供明確介面。 自動產生的介面稱為類別介面。 如需方針,請參閱類別介面簡介

    您可以使用 Visual Basic、C# 和 C++ 在程式碼中併入介面定義,而不需使用介面定義語言 (IDL) 或其對等項目。 如需語法詳細資訊,請參閱您的語言文件。

  • Managed 類型必須是公用的。

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

    Managed 類型會向可能未向 COM 公開的其他 Managed 程式碼公開功能。 例如,不會向 COM 用戶端公開參數化建構函式、靜態方法和常數欄位。 此外,執行階段將資料封送處理至類型或從中封送處理資料時,可能會複製或轉換資料。

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

    如果要讓 COM 看到公用類型的成員,則它們也必須是公用的。 您可以套用 ComVisibleAttribute,以限制組件、公用類型或公用類型的公用成員的可見性。 根據預設,會顯示所有公用類型和成員。

  • 型別必須具有要從 COM 啟用的公用無參數建構函式。

    COM 可以看到 Managed 公用類型。 不過,若沒有公用無參數建構函式 (沒有引數的建構函式),COM 用戶端就無法建立該型別。 如果該類型是透過一些其他方式啟用,則 COM 用戶端仍然可以使用它。

  • 類型不能是抽象的。

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

匯出至 COM 時,會壓平合併 Managed 類型的繼承階層。 Managed 與 Unmanaged 環境之間的版本設定不同。 向 COM 公開之類型與其他 Managed 類型的版本設定特性不同。

從 .NET 取用 COM 類型

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

  • 介面必須套用 ComImportAttribute
  • 介面必須套用 GuidAttribute 及 COM 介面的介面識別碼。
  • 介面應套用 InterfaceTypeAttribute 以指定此介面的基底介面類型 (IUnknownIDispatchIInspectable)。
    • 預設選項是具有 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

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

注意

WinRT 介面不會遵循此繼承模型。 它們的定義是遵循與 .NET 中以 [ComImport] 為基礎的 COM Interop 模型相同的模型。

使用 ComImportAttribute 的介面繼承

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

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

此程式碼不會表示「J 實作 I」。程式碼實際上表示:「實作 J 的任何類型也必須實作 I」。此差異會導致在以 ComImportAttribute 為基礎的 Interop unergonomic 中做出介面繼承的基本設計決策。 介面一律會被單獨考慮;介面的基底介面清單不會影響任何計算來判斷指定 .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 和更新版本)

GeneratedComInterfaceAttribute 所觸發的 COM 來源產生器會實作 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++ 範例,因此這些介面會正常運作。

另請參閱