时至今日,使用 COM 对象的正确方法是什么?

As of today, what is the right way to work with COM objects?

这是一个非常常见的问题,我决定提出这个问题,因为这个问题到今天可能会有不同的答案。希望这些答案将有助于理解什么是使用 COM 对象的正确方法。 就个人而言,在对这个问题发表不同意见后,我感到很困惑。

过去 5 年,我曾经使用 COM 对象,规则对我来说非常清楚:

  1. 在代码行中使用单个句点。使用多个句点会在幕后创建无法显式释放的临时对象。
  2. 不要使用 foreach,而是使用 for 循环并在每次迭代时释放每个项目
  3. 不要调用 FInalReleaseComObject,而是使用 ReleaseComObject。
  4. 不要使用 GC 来释放 COM 对象。 GC intent主要是调试使用
  5. 以创建对象的相反顺序释放对象。

有些人在阅读最后几行后可能会感到沮丧,这就是我所知道的如何正确 create/release Com 对象,我希望得到的答案会使它更清晰和无可争议。

以下是我在该主题上找到的一些链接。其中一些告诉需要调用 ReleaseComObject,而另一些则不需要。

"... In VSTO scenarios, you typically don’t ever have to use ReleaseCOMObject. ..."

"...You should use this method to free the underlying COM object that holds references..."

更新:

这个问题被标记为过于宽泛。根据要求,我会尽量简化并提出更简单的问题。

  1. 使用 COM 对象时是否需要 ReleaseComObject 或调用 GC 是正确的方法?
  2. VSTO 方法是否改变了我们过去使用 COM 对象的方式?
  3. 以上我写的规则,哪些是必须的,哪些是错误的?还有其他的吗?

.NET / COM 互操作设计精良,工作正常。特别是,.NET 垃圾收集器正确跟踪 COM 引用,并在 COM 对象没有剩余的运行时引用时正确释放它们。通过调用 Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...) 来干扰 COM 对象的引用计数是一种危险但常见的反模式。不幸的是,微软提出了一些不好的建议。

您的 .NET 代码可以在忽略所有 5 条规则的情况下正确地与 COM 交互。 如果您确实需要触发不再从运行时引用的 COM 对象的确定性清理,您可以安全地强制 GC(并可能等待终结器完成)。否则,您不必在代码中做任何特殊的事情来处理 COM 对象。

有一个重要的警告,可能会导致对垃圾收集器的作用产生混淆。调试 .NET 代码时,局部变量的生命周期人为地延长到方法的末尾,以支持在调试器下观察变量。这意味着您可能仍然拥有对 COM 对象的托管引用(因此 GC 不会清理),这比仅查看代码的预期形式要晚。此问题(仅在调试器下发生)的一个很好的解决方法是将 COM 调用的范围与 GC 清理调用分开。

例如,这里有一些与 Excel 交互并正确清理的 C# 代码。您可以粘贴到控制台应用程序中(只需添加对 Microsoft.Office.Interop.Excel 的引用):

using System;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;

namespace TestCsCom
{
    class Program
    {
        static void Main(string[] args)
        {
            // NOTE: Don't call Excel objects in here... 
            //       Debugger would keep alive until end, preventing GC cleanup

            // Call a separate function that talks to Excel
            DoTheWork();

            // Now let the GC clean up (repeat, until no more)
            do
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }
            while (Marshal.AreComObjectsAvailableForCleanup());
        }

        static void DoTheWork()
        {
            Application app = new Application();
            Workbook book = app.Workbooks.Add();
            Worksheet worksheet = book.Worksheets["Sheet1"];
            app.Visible = true;
            for (int i = 1; i <= 10; i++) {
                worksheet.Cells.Range["A" + i].Value = "Hello";
            }
            book.Save();
            book.Close();
            app.Quit();

            // NOTE: No calls the Marshal.ReleaseComObject() are ever needed
        }
    }
}

您会看到 Excel 进程正确关闭,表明所有 COM 对象都已正确清理。

VSTO 不会改变任何这些问题 - 它只是一个包装和扩展本机 Office COM 对象模型的 .NET 库。


关于这个问题有很多虚假信息和混淆,包括 MSDN 和 Whosebug 上的许多 post。

最终说服我仔细研究并找出正确建议的是这个 post https://blogs.msdn.microsoft.com/visualstudio/2010/03/01/marshal-releasecomobject-considered-dangerous/ 以及在 Whosebug 答案中发现在调试器下保持活跃的引用问题。


此一般指南的一个例外是 COM 对象模型要求以特定顺序发布接口。此处描述的 GC 方法无法让您控制 GC 释放 COM 对象的顺序。

我没有任何参考来表明这是否会违反 COM 合同。通常,我希望 COM 层次结构使用内部引用来确保对序列的任何依赖性得到适当管理。例如。在 Excel 的情况下,人们会期望 Range 对象保留对父 Worksheet 对象的内部引用,这样对象模型的用户就不需要显式地保持两者都有效。

在某些情况下,甚至 Office 应用程序也对 COM 对象的释放顺序敏感。一种情况似乎是使用 OLE 嵌入时 - 请参阅 https://blogs.msdn.microsoft.com/vsofficedeveloper/2008/04/11/excel-ole-embedding-errors-if-you-have-managed-add-in-sinking-application-events-in-excel-2/

因此,如果以错误的顺序释放对象,则创建的 COM 对象模型可能会失败,并且与此类 COM 模型的互操作需要更加小心,并且可能需要手动干预引用.

但对于与 Office COM 对象模型的一般互操作,我同意 VS blog post 调用 "Marshal.ReleaseComObject – a problem disguised as a solution"。