我在C#(ApplicationClass)中使用Excel互操作,并在finally子句中放置了以下代码:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。
我做错了什么,或者是否有其他方法可以确保正确处理互操作对象?
我目前正在研究Office自动化,并偶然发现了一个每次都适用于我的解决方案。它很简单,不涉及杀死任何进程。
似乎只要在当前的活动进程中循环,并以任何方式“访问”一个开放的Excel进程,Excel的任何游离挂起实例都将被删除。下面的代码只是检查名称为“Excel”的进程,然后将进程的MainWindowTitle属性写入字符串。与进程的这种“交互”似乎使Windows赶上并中止了冻结的Excel实例。
我在开发退出的加载项之前运行下面的方法,因为它会触发卸载事件。它每次都会删除所有挂起的Excel实例。老实说,我不完全确定为什么这样做,但它对我来说很好,可以放在任何Excel应用程序的末尾,而不必担心双点、Marshal.ReleaseComObject或杀死进程。我很想知道为什么这是有效的。
public static void SweepExcelProcesses()
{
if (Process.GetProcessesByName("EXCEL").Length != 0)
{
Process[] processes = Process.GetProcesses();
foreach (Process process in processes)
{
if (process.ProcessName.ToString() == "excel")
{
string title = process.MainWindowTitle;
}
}
}
}
正如一些人可能已经写过的,如何关闭Excel(对象)不仅重要;如何打开它以及项目类型也很重要。
在WPF应用程序中,基本上相同的代码在没有或很少有问题的情况下工作。
我有一个项目,在该项目中,同一个Excel文件针对不同的参数值被处理了多次-例如,基于通用列表中的值对其进行分析。
我将所有与Excel相关的函数放在基类中,将解析器放在一个子类中(不同的解析器使用通用的Excel函数)。我不希望Excel为泛型列表中的每个项再次打开和关闭,所以我只在基类中打开了一次,并在子类中关闭了它。我在将代码移动到桌面应用程序时遇到了问题。我已经尝试了上面提到的许多解决方案。以前已经实现了GC.Collect(),是建议的两倍。
然后我决定将打开Excel的代码移到一个子类中。现在我不再只打开一次,而是创建一个新对象(基类),为每个项目打开Excel并在最后关闭它。有一些性能损失,但根据几个测试,Excel进程关闭时没有问题(在调试模式下),因此临时文件也会被删除。如果我能得到一些更新,我会继续测试并写更多。
底线是:您还必须检查初始化代码,特别是如果您有许多类等。
首先,在执行Excel互操作时,您永远不必调用Marshal.ReleaseComObject(…)或Marshal.FinalReleaseComObject(.)。这是一个令人困惑的反模式,但任何有关此的信息(包括来自Microsoft的信息)都是不正确的,这些信息表明您必须从.NET手动释放COM引用。事实上,.NET运行时和垃圾收集器正确地跟踪和清理COM引用。对于您的代码,这意味着您可以删除顶部的整个`while(…)循环。
其次,如果要确保在进程结束时清理进程外COM对象的COM引用(以便Excel进程关闭),则需要确保垃圾收集器运行。您可以通过调用GC.Collect()和GC.WaitForPendingFinalizers()来正确地执行此操作。两次调用此操作是安全的,并且可以确保周期也被彻底清理(尽管我不确定是否需要,我希望能有一个示例来说明这一点)。
第三,当在调试器下运行时,本地引用将被人为地保持活动状态,直到方法结束(以便本地变量检查工作)。因此,GC.Collect()调用对于从同一方法中清除rng.Cells等对象无效。您应该将执行GC清理中的COM互操作的代码拆分为单独的方法。(这是我的一个关键发现,来自@nightcoder在这里发布的答案的一部分。)
因此,一般模式为:
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和Stack Overflow上的许多帖子(尤其是这个问题!)。
最终说服我仔细研究并找出正确建议的是博客文章Marshal.ReleaseComObject Considered Dangerous,以及发现在调试器下引用保持活动的问题,这让我之前的测试感到困惑。
我认为其中一些只是框架处理Office应用程序的方式,但我可能错了。在某些日子,一些应用程序会立即清理进程,而在其他日子,似乎要等到应用程序关闭。总的来说,我不再关注细节,只是确保一天结束时没有任何额外的流程。
还有,也许我过于简化了,但我想你可以。。。
objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();
正如我之前所说,我不太关注Excel进程出现或消失的细节,但这通常对我有用。我也不喜欢在最短的时间内保持Excel进程,但我可能只是在这方面有点偏执。