从枚举和其他参考方案中释放.NET中的COM对象

时间:2013-05-18 11:08:47

标签: c# .net reference interop ms-office

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

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]循环引用集合[book.Sheets]。是否会导致孤儿RCW计数?

这里有两个问题:

  • 在进行枚举时,最好的清理方法是什么?
  • [Sheets]中的[book.Sheets]等中间对象会发生什么变化,因为它们没有明确声明或清理过来?

更新:

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

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

将尝试汉斯的建议,并将其归还。

1 个答案:

答案 0 :(得分:6)

<强>枚举

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

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

并且,在释放引用之前,请注意离开循环的continuebreak语句。

<强>中间体

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

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中执行引用释放。

因此,在每个foreachfor循环中,您还需要使用try ... catch ... finally来确保释放枚举变量。

双打点

也不要"double dot" (only use a single period in lines of code)。在foreach中执行此操作是我们很容易犯的常见错误。如果我暂时不做非COM C#,我仍然会抓住自己这样做,因为由于LINQ样式表达式,将句点链接起来越来越常见。

双点打印的例子:

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

Phantom Excel

当然,最重要的考验是在程序退出后查看Excel是否在任务管理器中保持打开状态。如果是,那么你可能会打开一个COM引用。

<强>参考

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

强大的解决方案

上述参考之一提到了他用于foreach循环的助手。就个人而言,如果我做的不仅仅是一个简单的“脚本”项目,那么我将首先花时间开发一个专门为我的场景包装COM对象的库。我现在有一套共同的类,我可以重复使用,而且我发现在做其他事情之前投入设置它的时间远远超过了以后不必追捕未关闭的引用。自动化测试对于帮助实现这一目标也是必不可少的,并且可以为任何COM互操作获得奖励,而不仅仅是Excel。

每个COM对象(例如Sheet)将包装在实现IDisposable的类中。它将公开Sheets等属性,而WorkbookWrapper又具有索引器。一直跟踪所有权,最后如果您只是处理主对象(例如{{1}}),那么其他所有内容都会被内部处理掉。例如,跟踪添加工作表,以便处理新工作表。

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