當 COM 用戶端呼叫 .NET 物件時,Common Language Runtime 會為 物件建立 Managed 物件和 COM 可呼叫包裝函式 (CCW)。 無法直接參考 .NET 物件時,COM 用戶端會使用 CCW 作為受控物件的代理。
不論要求其服務的 COM 用戶端數目為何,執行階段會為受管理的物件建立恰好一個 CCW。 如下圖所示,多個 COM 用戶端可以保存公開 INew 介面之 CCW 的參考。 接著,CCW 會持有一個對實作介面的受控物件的單一參考,該物件會被垃圾回收。 COM 和 .NET 用戶端都可以同時對相同的 Managed 物件提出要求。
運行於 .NET 執行階段的其他類別無法看到 COM 可呼叫封裝器。 其主要目的是在托管和非托管程式碼之間協調呼叫;不過,CCW 也會管理其包裝的托管物件的物件身份和物件生命周期。
對象識別
執行階段會從垃圾回收堆中配置 .NET 物件的記憶體,能夠在需要時調整物件在記憶體中的位置。 相較之下,執行階段會從非收集的堆中分配 CCW 所需的記憶體,使 COM 客戶端能直接引用這個封裝。
物件存留期
與它所封裝的 .NET 用戶端不同,CCW 是以傳統 COM 的方式進行引用計數。 當 CCW 的參考計數達到零時,封裝器會在受控對象上釋放其參考。 在下一個垃圾收集周期期間,沒有剩餘參考的Managed物件會被收集。
模擬 COM 介面
CCW 會公開所有公用、COM 可見介面、數據類型,並將值以符合 COM 強制介面型互動的方式傳回 COM 用戶端。 針對 COM 用戶端,在 .NET 物件上叫用方法與在 COM 物件上叫用方法相同。
為了建立這種無縫方法,CCW 會製造傳統的 COM 介面,例如 IUnknown 和 IDispatch。 如下圖所示,CCW 會在所包裝的 .NET 物件上維護單一引用。 COM 用戶端和 .NET 對象都會透過CCW的 Proxy和存根建構彼此互動。
除了公開 Managed 環境中類別明確實作的介面之外,.NET 執行階段也會代表物件提供下表所列之 COM 介面的實作。 .NET 類別可以藉由提供自己的介面實作來覆寫預設行為。 不過,運行時間一律會提供 IUnknown 和 IDispatch 介面的實作。
| 介面 | 說明 |
|---|---|
| IDispatch | 提供晚期系結至 型別的機制。 |
| IErrorInfo | 提供錯誤的文字描述、其來源、說明檔案、說明上下文,以及定義錯誤之介面的 GUID(對 .NET 類別始終為 GUID_NULL)。 |
| IProvideClassInfo | 可讓 COM 用戶端存取 Managed 類別所實作的 ITypeInfo 介面。 在 .NET Core 上,對未從 COM 匯入的類型傳回COR_E_NOTSUPPORTED。 |
| ISupportErrorInfo | 可讓 COM 用戶端判斷 Managed 物件是否支援 IErrorInfo 介面。 如果是,可讓用戶端取得最新例外狀況物件的指標。 所有 Managed 類型都支援 IErrorInfo 介面。 |
| ITypeInfo (僅限.NET Framework) | 提供與 Tlbexp.exe所產生的類型資訊完全相同的類別類型資訊。 |
| IUnknown | 提供 IUnknown 介面的標準實作,COM 用戶端會管理 CCW 的存留期,並提供型別強制。 |
Managed 類別也可以提供下表所述的 COM 介面。
| 介面 | 說明 |
|---|---|
| (_classname) 類別介面 | 介面由運行時環境隱式公開,但未明確定義,它會將管理物件上所有明確公開的公用介面、方法、屬性和字段公開。 |
| IConnectionPoint 和 IConnectionPointContainer | 來源委派型事件的物件介面(用於註冊事件訂閱者的介面)。 |
| IDispatchEx (僅限.NET Framework) | 如果類別實作 IExpando,則執行階段所提供的介面。 IDispatchEx 介面是 IDispatch 介面的延伸模組,不同於 IDispatch,可啟用成員的列舉、新增、刪除和區分大小寫呼叫。 |
| IEnumVARIANT | 集合類型類別的介面,如果類別實作 IEnumerable,則會列舉集合中的物件。 |
類別介面簡介
類別介面在託管代碼中並未被明確定義,它是一個介面,負責公開 .NET 物件上所有明確公開的公用方法、屬性、欄位和事件。 此介面可以是雙重或僅限分派的介面。 類別介面會接收 .NET 類別本身的名稱,前面加上底線。 例如,針對類哺乳動物,類別介面是_Mammal。
對於衍生類別,類別介面也會公開基類的所有公用方法、屬性和字段。 衍生類別也會公開每個基類的類別介面。 例如,如果類別 Mammal 擴充類別 MammalSuperclass,其本身會擴充 System.Object,則 .NET 物件會向 COM 用戶端公開三個名為 _Mammal、_MammalSuperclass 和 _Object 的類別介面。
例如,請考慮下列 .NET 類別:
' Applies the ClassInterfaceAttribute to set the interface to dual.
<ClassInterface(ClassInterfaceType.AutoDual)> _
' Implicitly extends System.Object.
Public Class Mammal
Sub Eat()
Sub Breathe()
Sub Sleep()
End Class
// Applies the ClassInterfaceAttribute to set the interface to dual.
[ClassInterface(ClassInterfaceType.AutoDual)]
// Implicitly extends System.Object.
public class Mammal
{
public void Eat() {}
public void Breathe() {}
public void Sleep() {}
}
COM 客戶端可以取得名為 _Mammal 的類別介面的指標。 在 .NET Framework 上,您可以使用 類型連結庫導出工具 (Tlbexp.exe) 工具來產生包含 _Mammal 介面定義的類型庫。 .NET Core 不支援類型連結庫導出工具。 如果類別實作 Mammal 一或多個介面,介面會出現在coclass底下。
[odl, uuid(…), hidden, dual, nonextensible, oleautomation]
interface _Mammal : IDispatch
{
[id(0x00000000), propget] HRESULT ToString([out, retval] BSTR*
pRetVal);
[id(0x60020001)] HRESULT Equals([in] VARIANT obj, [out, retval]
VARIANT_BOOL* pRetVal);
[id(0x60020002)] HRESULT GetHashCode([out, retval] short* pRetVal);
[id(0x60020003)] HRESULT GetType([out, retval] _Type** pRetVal);
[id(0x6002000d)] HRESULT Eat();
[id(0x6002000e)] HRESULT Breathe();
[id(0x6002000f)] HRESULT Sleep();
}
[uuid(…)]
coclass Mammal
{
[default] interface _Mammal;
}
產生類別介面是選擇性的。 根據預設,COM Interop 會為您匯出至型別函式庫的每個類別產生僅限調度介面。 您可以藉由將 套用 ClassInterfaceAttribute 至類別,防止或修改此介面的自動建立。 雖然類別介面可以簡化將Managed類別公開給 COM 的工作,但其用途有限。
謹慎
使用類別介面,而不是明確定義您自己的介面,可能會使受控類別的未來版本設定複雜化。 請先閱讀下列指導方針,再使用類別介面。
為 COM 用戶端定義要使用的明確介面,而不是產生類別介面。
因為 COM Interop 會自動產生類別介面,因此對類別的後續版本變更可以改變 Common Language Runtime 所公開之類別介面的配置。 由於 COM 用戶端通常未準備處理介面版面配置中的變更,因此如果您變更 類別的成員配置,它們就會中斷。
此指導方針強化了公開給 COM 用戶端的介面必須維持不變的概念。 為了避免因不小心重新排列介面佈局而導致 COM 用戶端中斷,請透過明確定義介面,將類別的所有變更與介面佈局隔離開來。
使用 ClassInterfaceAttribute 來解除類別介面的自動產生,並實作 類別的明確介面,如下列代碼段所示:
<ClassInterface(ClassInterfaceType.None)>Public Class LoanApp
Implements IExplicit
Sub M() Implements IExplicit.M
…
End Class
[ClassInterface(ClassInterfaceType.None)]
public class LoanApp : IExplicit
{
int IExplicit.M() { return 0; }
}
ClassInterfaceType.None 值可防止類別元數據匯出至類型連結庫時產生類別介面。 在上述範例中,COM 用戶端只能透過 LoanApp 介面存取 IExplicit 類別。
避免快取分派識別碼 (DispIds)
使用類別介面是腳本用戶端、Microsoft Visual Basic 6.0 用戶端或任何未快取介面成員 DispId 之晚期綁定用戶端的可接受的選項。 DispId 會識別介面成員以啟用晚期系結。
針對類別介面,DispId 的產生是以介面中成員的位置為基礎。 如果您變更成員的順序,並將類別匯出至型別庫,您將改變類別介面中產生的 DispIds。
若要避免在使用類別介面時中斷晚期綁定 COM 用戶端,請使用 ClassInterfaceType.AutoDispatch 值套用 ClassInterfaceAttribute。 此值實現了僅限分派的類別介面,但省略了類型庫中的介面描述。 如果沒有介面描述,用戶端就無法在編譯時期快取 DispIds。 雖然這是類別介面的預設介面類型,但您可以明確套用屬性值。
<ClassInterface(ClassInterfaceType.AutoDispatch)> Public Class LoanApp
Implements IAnother
Sub M() Implements IAnother.M
…
End Class
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class LoanApp
{
public int M() { return 0; }
}
為了在執行時取得介面成員的 DispId,COM 用戶端可以呼叫 IDispatch.GetIdsOfNames。 若要在介面上叫用方法,請將傳回的 DispId 當做自變數傳遞至 IDispatch.Invoke。
請限制類別介面的雙界面選項的使用。
雙重介面可讓 COM 用戶端對接口成員進行早期和晚期系結。 在設計時間及測試期間,您可能會發現將類別介面設定為雙重很有用。 對於永遠不會修改的Managed類別(及其基類),這個選項也是可接受的。 在其他所有情況下,請避免將類別介面設定為雙重。
在極少數情況下,自動生成的雙重介面可能很合適;不過,更常會造成版本上的複雜性。 例如,使用衍生類別介面的 COM 用戶端,可以輕鬆地中斷基類的變更。 當第三方提供基類時,類別介面的配置會脫離您的控制。 此外,不同於僅僅分派介面,雙重介面(ClassInterfaceType.AutoDual)會在導出的類型庫中提供類別介面的描述。 這種類型的描述鼓勵後期綁定的用戶端在編譯時快取 DispIds。
請確保所有 COM 事件通知都是後期綁定。
根據預設,COM 類型資訊會直接內嵌到託管組件中,這樣就不需要主要互操作程序集(PIA)。 不過,內嵌類型資訊的其中一個限制是,它不支援透過早期綁定的 vtable 呼叫傳遞 COM 事件通知,但只支援晚期綁定 IDispatch::Invoke 呼叫。
如果您的應用程式需要對 COM 事件介面方法進行早期系結呼叫,您可以將 Visual Studio 中的 Embed Interop Types 屬性設定為 true,或在專案檔中包含下列元素:
<EmbedInteropTypes>True</EmbedInteropTypes>