使用IDisposable清理Excel Interop对象

46

在我的公司中,释放Excel Interop对象的常见方法是使用IDisposable,具体方法如下:

Public Sub Dispose() Implements IDisposable.Dispose
    If Not bolDisposed Then
        Finalize()
        System.GC.SuppressFinalize(Me)
    End If
End Sub

Protected Overrides Sub Finalize()
    _xlApp = Nothing
    bolDisposed = True
    MyBase.Finalize()
End Sub

其中 _xlApp 是在构造函数中以下方式创建的:

Try
    _xlApp = CType(GetObject(, "Excel.Application"), Excel.Application)
Catch e As Exception
    _xlApp = CType(CreateObject("Excel.Application"), Excel.Application) 
End Try

客户端使用 using-statement 来执行关于Excel Interop对象的代码。

我们完全避免使用 两个点规则。现在,我开始研究如何释放(Excel)Interop对象,几乎所有讨论都像How to properly clean up excel interop objectsRelease Excel Objects一样,主要使用Marshal.ReleaseComObject(),没有使用IDisposable接口。

我的问题是:使用IDisposable接口释放Excel Interop对象是否有任何缺点?如果有,这些缺点是什么。


尽管如此,实现Dispose/Finalize的常见方式是相反的,即由终结器调用Dispose方法。 - Lasse V. Karlsen
2个回答

146

使用IDisposable接口有什么缺点吗?

当然了,它完全没有任何作用。使用Using或调用Dispose()从来不是将变量设置为Nothing的适当方式。这就是你的代码所做的全部。

我们完全避免使用两个点规则。

随便忽略它,这是无意义的,只会带来麻烦。博客作者暗示的断言是,这样做会迫使程序员使用变量来存储xlApp.Workbooks的值。这样,稍后,他就有了保留一个对象以确保调用releaseObject()的机会。但是还有很多其他产生接口引用的语句不使用点号,例如Range(x,y),那里有一个隐藏的Range对象引用,你永远看不到它。而必须将它们存储下来只会产生非常复杂的代码。

而且,只要忽略一个就足以彻底失败。这是无法调试的。这种代码是C程序员必须编写的代码。而且往往会惨败,大型C程序经常泄漏内存,他们的程序员花费大量时间寻找这些泄漏。当然,这并不是.NET的方式,它有一个垃圾收集器可以自动执行此操作。它从不出错。

问题是,它在处理工作时有点慢。非常明显是故意这样设计的。除了在这种代码中,没有人会注意到这一点。你可以看到垃圾回收器没有运行,你还能看到Office程序正在运行。当你写了xlapp.Quit()时,它并没有退出,在任务管理器的进程选项卡中仍然存在。他们希望发生的是,当他们说要退出时,它就应该退出。

在.NET中,这很有可能实现,你可以强制GC完成工作:

GC.Collect()
GC.WaitForPendingFinalizers()

嘭!每个Excel对象引用都会自动释放。您无需自己存储这些对象引用并显式调用Marshal.ReleaseComObject(),CLR会为您完成。而且它永远不会出错,它不使用或需要“双点规则”,也没有找回那些隐藏接口引用的麻烦。


但是很重要的一点是确切地在哪里放置此代码。大多数程序员将其放在使用这些Excel接口的同一方法中,这很好,但在调试代码时却行不通,这是一个怪癖,在此答案中有解释。博客作者代码的正确方式是将代码移动到一个小帮助器方法中,让我们称之为DoExcelThing()。像这样:

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    DoExcelThing()
    GC.Collect()
    GC.WaitForPendingFinalizers()
    '' Excel.exe no longer running anymore at this point
End Sub

请记住,这实际上只是一个调试工具。程序员们讨厌不得不使用任务管理器来终止僵尸Excel.exe实例。当他们停止调试器时,程序变成了僵尸状态,无法正常退出并收集垃圾。这是正常的。当您的程序因任何原因在生产中死亡时,也会发生这种情况。把你的精力放在正确的地方,修复你代码中的错误,以便你的程序不会死掉。垃圾回收程序不需要更多帮助。


25
阿门!请继续努力对抗整个 ReleaseComObject 神秘操作,它已经侵扰了互联网(指出调试诡异行为再加一分)。 - Govert
3
感谢您澄清了这个问题,即不需要使用“ no-two-dot, Marshal.ReleaseComObject” 的方式来处理Excel(或大多数其他COM)Interop并释放引用。我之前听到过很多关于它的信息,担心我必须重新编写代码库的大部分内容... - Mike
3
整个 RCW 设计已经烂大街了。未来会是自动生成的 C++/CLI 封装程序集(增强互操作程序集),无论你是使用 Dispose 还是将其留给 GC,它都会正确释放。设计不良导致每个 C# 办公自动化都不稳定,导致每个人都逃离 COM,并最终导致每个人都逃离 Office 套件本身。现在为时已晚,这已经发生了。 - rwong
4
在第一次阅读这篇文章时,我错过了关于我应该在哪里放置GC.Collect()命令的部分。在将其放置在正确的位置后,它完美地运行了。 - Cornelius
1
我有点困惑...我从来没有任何Excel的僵尸实例留在那儿。当我告诉我的老板我正在将数据写入Excel时,他过来向我展示,并检查了任务管理器中的进程和应用程序。我的问题是:截至VS2015和.NET 4.6,GC是否需要额外的帮助来清理COM对象,这仍然是一个问题吗? - jpcguy89
显示剩余6条评论

1

如果你正在寻找一种更简洁的方法,你可以使用Koogra。它不需要太多额外的开销(只需在引用中包含两个DLL),而且你不必处理隐式垃圾回收。

下面的代码是从一个Excel文件中读取10行10列所需的全部内容,而不会留下任何EXCEL32Excel.exe进程。我一个月前遇到了这个问题,并写了如何解决。比直接处理Excel Interop要容易得多,也更加简洁。

Koogra.IWorkbook workbook = Koogra.WorkbookFactory.GetExcel2007Reader("MyExcelFile.xlsx");
Net.SourceForge.Koogra.IWorksheet worksheet = workbook.Worksheets.GetWorksheetByName("Sheet1");

//This will invididually print out to the Console the columns A-J (10 columns) for rows 1-10.
for (uint rowIndex = 1; rowIndex <= 10; rowIndex++)
{
    for (uint columnIndex = 1; columnIndex <= 10; columnIndex++)
    {
        Console.WriteLine(worksheet.Rows.GetRow(rowIndex).GetCell(columnIndex).GetFormattedValue());
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接