我在C#(ApplicationClass)中使用Excel互操作,并在finally子句中放置了以下代码:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。
我做错了什么,或者是否有其他方法可以确保正确处理互操作对象?
到目前为止,似乎所有的答案都涉及其中一些:
终止进程使用GC.Collect()跟踪每个COM对象并正确释放它。
这让我意识到这个问题有多么困难:)
我一直在开发一个库来简化对Excel的访问,我正在努力确保使用它的人不会留下一片混乱(手指交叉)。
我没有直接在Interop提供的接口上进行编写,而是使用扩展方法来简化工作。类似于ApplicationHelpers.CreateExcel()或工作簿.CreateWorksheet(“mySheetNameThatWillBeValidated”)。自然,任何创建的东西都可能会在以后的清理中导致问题,所以我实际上更倾向于在最后的手段中终止这个过程。然而,正确清理(第三种选择)可能是破坏性最小、控制性最强的。
因此,在这种情况下,我想知道这样做是否不是最好的:
public abstract class ReleaseContainer<T>
{
private readonly Action<T> actionOnT;
protected ReleaseContainer(T releasible, Action<T> actionOnT)
{
this.actionOnT = actionOnT;
this.Releasible = releasible;
}
~ReleaseContainer()
{
Release();
}
public T Releasible { get; private set; }
private void Release()
{
actionOnT(Releasible);
Releasible = default(T);
}
}
我用“不可行”来避免与一次性使用混淆。但将其扩展到IDisposable应该很容易。
这样的实现:
public class ApplicationContainer : ReleaseContainer<Application>
{
public ApplicationContainer()
: base(new Application(), ActionOnExcel)
{
}
private static void ActionOnExcel(Application application)
{
application.Show(); // extension method. want to make sure the app is visible.
application.Quit();
Marshal.FinalReleaseComObject(application);
}
}
可以对所有类型的COM对象执行类似的操作。
在工厂方法中:
public static Application CreateExcelApplication(bool hidden = false)
{
var excel = new ApplicationContainer().Releasible;
excel.Visible = !hidden;
return excel;
}
我希望每个容器都会被GC正确地销毁,因此会自动调用Quit和Marshal.FinalReleaseComObject。
评论?或者这是对第三类问题的回答?
为了说明Excel不关闭的原因,即使在读取和创建时为每个对象创建直接引用,也需要使用“For”循环。
For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot
objWorkBook.Close 'or whatever
FinalReleaseComObject(objWorkBook)
objWorkBook = Nothing
Next
'The above does not work, and this is the workaround:
For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count
Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)
objTempWorkBook.Saved = True
objTempWorkBook.Close(False, Type.Missing, Type.Missing)
FinalReleaseComObject(objTempWorkBook)
objTempWorkBook = Nothing
Next
首先,在执行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,以及发现在调试器下引用保持活动的问题,这让我之前的测试感到困惑。
这似乎太复杂了。根据我的经验,要让Excel正确关闭,只有三件关键的事情:
1:确保没有对您创建的excel应用程序的剩余引用(无论如何,您应该只有一个引用;将其设置为空)
2:调用GC.Collect()
3:必须通过用户手动关闭程序或通过对Excel对象调用“退出”来关闭Excel。(请注意,“退出”的功能与用户试图关闭程序的功能相同,如果存在未保存的更改,即使Excel不可见,也会显示一个确认对话框。用户可以按“取消”,Excel将不会关闭。)
1需要在2之前发生,但3可以随时发生。
实现这一点的一种方法是用自己的类包装interop Excel对象,在构造函数中创建interop实例,并使用Dispose实现IDisposable
if (!mDisposed) {
mExcel = null;
GC.Collect();
mDisposed = true;
}
这将从程序的方面清理出优秀的东西。一旦Excel关闭(由用户手动或您调用退出),该过程将消失。如果程序已经关闭,那么进程将在GC.Collect()调用中消失。
(我不确定它有多重要,但您可能需要在GC.Collect()调用之后调用GC.WaitForPendingFinalizers(),但这并不是完全需要摆脱Excel进程。)
多年来,这对我来说毫无问题。请记住,虽然这是有效的,但实际上您必须优雅地关闭它才能工作。如果在清理excel之前中断程序(通常在调试程序时单击“停止”),则仍会累积excel.exe进程。