为交互操作生成 COM 组件

更新:2007 年 11 月

如果计划在将来编写基于 COM 的应用程序,则可以设计代码使其与托管代码有效地交互操作。借助于高级规划,还可以简化由非托管代码到托管代码的移植。

以下建议总结了用于编写与托管代码进行交互的 COM 类型的最佳做法。

提供类型库

在大多数情形下,公共语言运行库需要所有类型的元数据,包括 COM 类型。包含在 Windows 软件开发工具包 (SDK) 中的类型库导入程序 (Tlbimp.exe) 可以将 COM 类型库转换为 .NET Framework 元数据。类型库转换为元数据后,托管客户端可以无缝地调用 COM 类型。为了易于使用,请始终在类型库中提供类型信息。

可以将类型库包装为单独的文件或将其作为资源嵌入到 .dll、.exe 或 .ocx 文件中。还可以直接生成元数据,这使您可以用出版商的密钥对对元数据进行签名。用密钥签名的元数据具有确定的源,可以在调用方的密钥不正确时阻止绑定,从而提高了安全性。

注册类型库

若要正确地封送调用,运行库可能需要查找描述特定类型的类型库。除后期绑定的情况外,类型库必须在运行库可以查看它之前注册。

通过将 regkind 标志设置为 REGKIND_REGISTER 来调用 Microsoft Win32 API LoadTypeLibEx 函数,可以注册类型库。Regsvr32.exe 自动注册嵌入到 .dll 文件中的类型库。

使用安全数组而不是变长数组

COM 安全数组具有自我描述性。通过检查安全数组,运行库封送拆收器就可以在运行时确定数组的秩、大小、界限,并且通常可以确定数组内容的类型。变长(或 C 样式)数组不具有相同的自我描述性质。例如,除了提供元素类型外,以下非托管方法签名未提供有关数组参数的任何信息。

HRESULT DoSomething(int cb, [in] byte buf[]);

实际上,该数组不能与通过引用传递的任何其他参数相区分。因此,Tlbimp.exe 不会转换 DoSomething 方法的数组参数。相反,该数组将显示为对 Byte 类型的引用,如以下代码所示。

Public Sub DoSomething(cb As Integer, ByRef buf As Byte)
public void DoSomething(int cb, ref Byte buf);

为提高交互操作,可以在非托管方法签名中将参数作为 SAFEARRAY 键入。例如:

HRESULT DoSomething(SAFEARRAY(byte)buf);

Tlbimp.exe 将 SAFEARRAY 转换为以下托管数组类型:

Public Sub DoSomething(buf As Byte())
public void DoSomething(Byte[] buf);

使用符合自动化的数据类型

运行库封送处理服务自动支持所有符合自动化的数据类型。不一定支持不符合的类型。

在类型库中提供版本和区域设置

在导入类型库时,类型库版本和区域设置信息也会被传播到程序集中。然后,托管客户端就可以绑定到该程序集的特定版本或区域设置,或者绑定到该程序集的最新版本。在类型库中提供版本信息使客户端能够准确选择要使用的程序集版本。

使用可直接复制到本机结构中的类型

数据类型或者是可直接复制到本机结构中的类型,或者是非直接复制到本机结构中的类型。可直接复制到本机结构中的类型具有跨 interop 边界的通用表示形式。整数和浮点类型是可直接复制到本机结构中的类型。可直接复制到本机结构中的类型的数组和结构也是可直接复制到本机结构中的。字符串、日期和对象是在封送处理过程中转换的非直接复制到本机结构中的类型的示例。

可直接复制到本机结构中的类型和非直接复制到本机结构中的类型都受到 interop 封送处理服务的支持;但在封送处理期间需要转换的类型的性能不像可直接复制到本机结构中的类型那样好。如果使用非直接复制到本机结构中的类型,请注意将增加与对其进行封送处理相关联的系统开销。

字符串特别容易出现问题。托管字符串存储为 Unicode 字符,因此可以更有效地封送到需要 Unicode 字符参数的非托管代码。如果可能,最好避免使用由 ANSI 字符组成的字符串。

实现 IProvideClassInfo

将非托管接口封送到托管代码时,运行库将创建特定类型的包装。方法签名通常指示该接口的类型,但实现该接口的对象的类型可能是未知的。如果对象的类型是未知的,则运行库将使用比类型特定的包装功能较弱的一般 COM 对象包装来包装该接口。

例如,考虑以下 COM 方法签名:

interface INeedSomethng {
   HRESULT DoSomething(IBiz *pibiz);
}

在导入时,该方法将按如下方式转换:

Interface INeedSomething
   Sub DoSomething(pibiz As IBiz)
End Interface
interface INeedSomething {
   void DoSomething(IBiz pibiz);
}

如果将实现 INeedSomething 接口的托管对象传递给 IBiz 接口,则 interop 封送拆收器将尝试在首次将 IBiz 引入到托管代码时使用特定类型的对象包装来包装该接口。若要标识正确类型的包装,封送拆收器必须知道实现该接口的对象的类型。封送拆收器尝试确定对象类型的一种方法是查询 IProvideClassInfo 接口。如果对象实现 IProvideClassInfo,则封送拆收器将确定该对象的类型,并将该接口包装在类型化的包装中。

使用模块调用

在托管和非托管代码之间封送数据会产生一定的开销。通过减少跨边界的转换可以减小这种开销。可使转换数减为最小的接口通常比经常跨越边界而在每一次跨越时执行较小任务的接口的性能更好。

谨慎使用指示失败的 HRESULT

当托管客户端调用 COM 对象时,运行库将 COM 对象的指示失败的 HRESULT 映射为异常,封送拆收器在从调用返回时引发该异常。托管异常模型已针对非异常情形进行了优化;在没有异常发生时,几乎不存在任何与捕获异常关联的系统开销。相反,当异常确实发生时,捕获异常可能会产生极高的开销。

请尽量少用异常,并避免为获取信息而返回指示失败的 HRESULT。请为异常情形保留指示失败的 HRESULT。要意识到过度使用指示失败的 HRESULTS 可能会影响性能。

显式释放外部资源

某些对象在其生存期内使用外部资源;例如,数据库连接可能会更新记录集。通常,对象在其生存期内会占据外部资源,而显式释放则可以立即返回该资源。例如,您可以针对文件对象使用 Close 方法,而不是在类析构函数中关闭该文件或者使用 IUnknown.Release。通过在代码中提供 Close 方法的等效项,可以释放外部文件资源,即使该文件对象继续存在也是如此。

避免重定义非托管类型

在托管代码中实现现有 COM 接口的正确方法是从使用 Tlbimp.exe 或等效的 API 导入接口的定义开始。得到的元数据将提供 COM 接口的兼容定义(相同的 IID、相同的 DispIds 等)。

避免在托管代码中手动重定义 COM 接口。这一任务将耗费时间,而且很少产生与现有 COM 接口兼容的托管接口。请改用 Tlbimp.exe 来维护定义的兼容性。

避免使用指示成功的 HRESULT

捕获异常是托管应用程序处理错误情形的最自然的方式。若要使对 COM 类型的使用透明,运行库就要在每次 COM 方法返回指示失败的 HRESULT 时自动引发异常。

如果 COM 对象返回指示成功的 HRESULT,则运行库将返回 retval 参数中的任何值。默认情况下,此 HRESULT 被放弃,这就使托管客户端很难检查指示成功的 HRESULT 的值。虽然可以使用 PreserveSigAttribute 属性保留 HRESULT,但该过程需要努力才能实现。您必须手动将该属性添加到用 Tlbimp.exe 或等效的 API 生成的程序集中。

只要可能,最好避免使用指示成功的 HRESULT。您可以改用 Out 参数返回有关调用状态的信息。

避免使用模块函数

类型库可以包含在模块上定义的函数。通常,使用这些函数来提供 DLL 入口点的类型信息。Tlbimp.exe 不会导入这些函数。

避免在默认接口中使用 System.Object 的成员

托管客户端和 COM coclass 与运行库所提供的包装帮助进行交互。导入 COM 类型时,转换过程将 coclass 的默认接口的所有方法添加到包装类中,该包装类是从 System.Object 类派生的。为默认接口的成员命名时要小心,以免与 System.Object 的成员发生命名冲突。如果发生冲突,则导入的方法将重写基类方法。

如果默认接口的方法和 System.Object 的方法提供相同的功能,则此操作可能是有利的。但如果以预料不到的方式使用默认接口的方法,则可能会出现问题。若要防止命名冲突,请避免在默认接口中使用下列名称:Object、Equals、Finalize、GetHashCode、GetType、MemberwiseClone 和 ToString。

请参见

参考

类型库导入程序 (Tlbimp.exe)

其他资源

互操作的设计注意事项