6

我有以下代码(从在线教程获得)。该代码正在运行,但我怀疑处理 Excel com 对象的方式有些不合适。我们真的需要调用 GC.Collect 吗?或者处理这个 Excel com 对象的最佳方法是什么?

Public Sub t1()
    Dim oExcel As New Excel.Application
    Dim oBook As Excel.Workbook = oExcel.Workbooks.Open(TextBox2.Text)

    'select WorkSheet based on name
    Dim oWS As Excel.Worksheet = CType(oBook.Sheets("Sheet1"), Excel.Worksheet)
    Try

        oExcel.Visible = False
        'now showing the cell value
        MessageBox.Show(oWS.Range(TextBox6.Text).Text)

        oBook.Close()
        oExcel.Quit()

        releaseObject(oExcel)
        releaseObject(oBook)
        releaseObject(oWS)
    Catch ex As Exception
        MsgBox("Error: " & ex.ToString, MsgBoxStyle.Critical, "Error!")
    End Try
End Sub

Private Sub releaseObject(ByVal obj As Object)
    Try
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj)
        obj = Nothing
    Catch ex As Exception
        obj = Nothing
    Finally
        GC.Collect()
    End Try
End Sub
4

4 回答 4

8

首先 - 您永远不必打电话Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...)在进行 Excel 互操作时。这是一个令人困惑的反模式,但任何有关此的信息(包括来自 Microsoft 的)表明您必须从 .NET 手动释放 COM 引用都是不正确的。事实上,.NET 运行时和垃圾收集器正确地跟踪和清理 COM 引用。对于您的代码,这意味着您可以删除整个releaseObject(...)Sub 并调用它。

其次,如果要确保在进程结束时清理对进程外 COM 对象的 COM 引用(以便 Excel 进程将关闭),则需要确保垃圾收集器运行。您可以通过调用GC.Collect()和正确地做到这一点GC.WaitForPendingFinalizers()。调用两次是安全的, end 确保循环也被清理干净。

第三,在调试器下运行时,局部引用会人为地保持活动状态,直到方法结束(这样局部变量检查才起作用)。因此,调用对于从同一方法GC.Collect()中清除对象无效。rng.Cells您应该将执行 COM 互操作的代码从 GC 清理中拆分为单独的方法。

一般模式是:

Sub WrapperThatCleansUp()

    ' 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 (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

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

最终说服我仔细研究并找出正确建议的是这篇文章https://blogs.msdn.microsoft.com/visualstudio/2010/03/01/marshal-releasecomobject-considered-dangerous/以及找到在某些 StackOverflow 答案的调试器下,引用保持活动状态的问题。

于 2016-06-29T22:30:21.213 回答
6

@PanPizza C# 和 VB.NET 非常相似,去掉;行尾的 ,Worksheets sheets = ...变成Dim sheets Worksheets = .... 如果您有兴趣在编程方面做得更好,那么您应该真正学习如何在这两种方法之间进行转换,因为许多 .NET 示例仅在其中一种中提供,而您实际上是在限制自己。

如本答案所述:如何正确清理 Excel 互操作对象?“从不使用两个点”这意味着总是降级到单个子对象并且从不这样做Dim oWS AS Excel.Worksheet = oExcel.Worksheets.Open(...)总是降级到工作簿然后降级到工作表,从不直接从Excel.Application.

作为一般规则,您需要做的是以与创建项目相反的顺序释放项目。否则,您将脚从其他参考下方取出,它们将无法正确解除分配。

请注意如何创建 Excel 应用程序 ( oExcel),然后是 Excel 工作簿 ( oBook),最后是 Excel 工作表 ( oWS),您需要以相反的顺序释放它们。

因此,您的代码变为:

    oBook.Close()
    oExcel.Quit()

    releaseObject(oWS)
    releaseObject(oBook)
    releaseObject(oExcel)
Catch ex As Exception

并从Sub releaseObject(ByVal obj As Object)

Finally
    GC.Collect()

这不是必需的,GC 会自然而然地发生并且不要期望您的应用程序立即释放内存,.NET 将未分配的内存池化,以便它可以轻松地在此内存中实例化对象,而不必向操作系统请求更多内存。

于 2012-04-25T06:10:38.973 回答
1

对我来说,关键是让 GarbageCollector (GC) 知道我想要清理一些东西。我意识到这通常不是必需的,但是在使用 COM 对象时,有时是必需的。有关更多信息,请参阅此链接https://www.add-in-express.com/creating-addins-blog/2013/11/05/release-excel-com-objects/

释放对象后,通过调用和 请求GC清理。上面的链接指出,为了从内存中完全删除 COM 对象,必须调用这些方法两次。就我而言,调用这些方法一次可行,但可能值得调用两次。Collect()WaitForPendingFinalizers()

oBook.Close()
oExcel.Quit()

releaseObject(oExcel)
releaseObject(oBook)
releaseObject(oWS)

GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()
GC.WaitForPendingFinalizers()
于 2015-05-21T16:05:07.270 回答
0

我已经搜索并搜索了这个,甚至微软自己的解决方案也不起作用(如果你想看看这里)。我有一个将数据导出到 Excel 模板的 vb.net 应用程序。理想情况下,当用户关闭 Excel 窗口时,它会终止该进程,但它不会,因为正如 Microsoft 文章中所述,vb.net 仍在引用它。

您需要自己杀死该进程,有一个过程如下:

For Each p As Process In Process.GetProcesses
     If p.ProcessName = "EXCEL.EXE" Then p.Kill
Next

但是,这会杀死所有 Excel 实例,并且用户可能会打开其他 Excel 窗口,这些窗口会在不保存的情况下关闭,所以我想出了这个(我正在使用的工作簿称为“Top 5 Issues Template”):

For Each p As Process In Process.GetProcesses
     If InStr(p.MainWindowTitle, "Top 5 Issues Template") <> 0 Then p.Kill
Next

这通过窗口名称而不是进程名称来查找,并且仅终止与其相关的进程。这是我可以让 Excel 正确关闭而不会弄乱任何东西的唯一方法。

于 2013-09-13T08:38:12.417 回答