4

我有一个使用 COM Interop 连接到 Microsoft Office 应用程序的 WinForms 应用程序。我已经阅读了大量有关如何正确处理 COM 对象的材料,这是我的应用程序中使用 Microsoft 自己的文章(此处)中的技术的典型代码:

Excel.Application excel = new Excel.Application();
Excel.Workbook book = excel.Workbooks.Add();
Excel.Range range = null;

foreach (Excel.Worksheet sheet in book.Sheets)
{
    range = sheet.Range["A2:Z2"];

    // Process [range] here.
    range.MergeCells();

    System.Runtime.InteropServices.Marshal.ReleaseComObject(range);
    range = null;
}

// Release explicitly declared objects in hierarchical order.
System.Runtime.InteropServices.Marshal.ReleaseComObject(book);
System.Runtime.InteropServices.Marshal.ReleaseComObject(excel);

book = null;
excel = null;

// As taken from:
//   http://msdn.microsoft.com/en-us/library/aa679807(v=office.11).aspx.
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
System.GC.Collect();
System.GC.WaitForPendingFinalizers();

所有异常处理都已被剥离,以使该问题的代码更清晰。

循环中的[sheet]对象会发生什么?[foreach]据推测,它不会被清理,我们也不能在它被枚举的时候对其进行篡改。一种替代方法是使用索引循环,但这会产生丑陋的代码,并且 Office 对象库中的某些结构甚至不支持索引。

此外,[foreach]循环引用了 collection [book.Sheets]。这是否也留下了孤立的 RCW 计数?

所以这里有两个问题:

  • 需要枚举时最好的清理方法是什么?
  • [Sheets]像in这样的中间对象[book.Sheets]没有显式声明或清理,它们会发生什么?

更新:

我对 Hans Passant 的建议感到惊讶,并认为有必要提供一些背景信息。

这是客户端/服务器应用程序,客户端连接到许多不同的 Office 应用程序,包括 Access、Excel、Outlook、PowerPoint 和 Word 等。它有超过 1,500 个类(并且还在增加),用于测试最终用户正在执行的某些任务,并在培训模式下模拟它们。它用于培训和测试学生在学术环境中的 Office 熟练程度。由于有多个开发人员和大量的类,因此很难实施对 COM 友好的编码实践。我最终求助于结合使用反射和源代码解析来创建自动化测试,以确保这些类在代码前审查阶段的完整性。

会尝试一下 Hans 的建议并回复。

4

1 回答 1

6

枚举

您的sheet循环变量确实没有被释放。在为 excel 编写互操作代码时,您必须经常观察您的 RCW。foreach我倾向于使用枚举而不是使用枚举,for因为它让我意识到每当我通过必须显式声明变量来获取引用时。如果您必须枚举,那么在循环结束时(在您离开循环之前)执行以下操作:

if (Marshal.IsComObject(sheet)) {
    Marshal.ReleaseComObject(sheet);
}

并且,在您发布参考之前,请注意离开循环的语句continuebreak

中间体

这取决于中间对象是否实际上是一个 COM 对象 ( book.Sheetsis),但如果是,那么您需要首先在字段中获取对它的引用,然后枚举该引用,然后确保您处理该字段。否则你基本上是“双点”(见下文):

using xl = Microsoft.Office.Interop.Excel;
...
public void DoStuff () {
    ...
    xl.Sheets sheets = book.Sheets;
    bool sheetsReleased = false;
    try {
        ...
        foreach (xl.Sheet in sheets) { ... try, catch and dispose of sheet ... }
        ... release sheets using Marshal.ReleaseComObject ...
        sheetsDisposed  = true;
    }
    catch (blah) { ... if !sheetsDisposed , dispose of sheets ... }
}

上面的代码是一般模式(如果你完整输入它会很长,所以我只关注重要部分)

错误呢?

在使用try ... catch ... finally. 确保您非常小心地使用它。finally 在堆栈溢出,内存不足,安全异常等情况下并不总是被调用,所以如果你想确保你清理,并且如果你的代码崩溃,不要让幻像 excel 实例打开,那么你必须有条件地执行在抛出异常之前在 catch 中释放引用。

因此,在every foreachorfor循环内部,还需要使用try ... catch ... finally确保枚举变量被释放。

双点

也不要“双点”(仅在代码行中使用单个句点)。这样做foreach是一个常见的错误,我们很容易做到。如果我已经停止使用非 COM C# 一段时间,我仍然会发现自己这样做,因为由于 LINQ 样式表达式,将句点链接在一起变得越来越普遍。

双点示例:

  • item.property.propertyIWant
  • item.Subcollection[0](您在调用该子集合上的索引器属性之前调用 SubCollection)
  • foreach x in y.SubCollection(本质上你是在打电话SubCollection.GetEnumerator,所以你又是“双点”)

幻影Excel

当然,最大的测试是查看程序退出后 Excel 是否在任务管理器中保持打开状态。如果是这样,那么您可能打开了一个 COM 引用。

参考

您说您已经对此进行了大量研究,但如果它有帮助,那么我发现有一些有用的参考资料是:

强大的解决方案

上述参考资料之一提到了他用于foreach循环的助手。就个人而言,如果我要做的不仅仅是一个简单的“脚本”项目,那么我将首先花时间开发一个库,专门为我的场景包装 COM 对象。现在我有一组通用的类可以重用,而且我发现在做任何其他事情之前花在设置上的时间比以后不必寻找未关闭的引用要多得多。自动化测试对于帮助实现这一点也很重要,并且可以从任何 COM 互操作中获得回报,而不仅仅是 Excel。

每个 COM 对象,例如Sheet,都将包装在一个实现IDisposable. 它将公开属性,例如Sheetswhich 又具有索引器。所有权一直被跟踪,最后,如果您简单地处置主对象,例如WorkbookWrapper,那么其他一切都会在内部处置。例如,添加工作表会被跟踪,因此新工作表也将被处理掉。

虽然这不是万无一失的方法,但您至少可以在 95% 的用例中依赖它,而另外 5% 的用例您完全了解并在代码中处理好。最重要的是,一旦您第一次完成它,它就会经过测试和重复使用。

于 2013-05-18T12:31:52.490 回答