我在C#(ApplicationClass)中使用Excel互操作,并在finally子句中放置了以下代码:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。
我做错了什么,或者是否有其他方法可以确保正确处理互操作对象?
首先,在执行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进程,但我可能只是在这方面有点偏执。
我找到了一个有用的通用模板,它可以帮助实现COM对象的正确处置模式,这些对象在超出范围时需要调用Marshal.ReleaseComObject:
用法:
using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
try
{
using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
{
// do something with your workbook....
}
}
finally
{
excelApplicationWrapper.ComObject.Quit();
}
}
模板:
public class AutoReleaseComObject<T> : IDisposable
{
private T m_comObject;
private bool m_armed = true;
private bool m_disposed = false;
public AutoReleaseComObject(T comObject)
{
Debug.Assert(comObject != null);
m_comObject = comObject;
}
#if DEBUG
~AutoReleaseComObject()
{
// We should have been disposed using Dispose().
Debug.WriteLine("Finalize being called, should have been disposed");
if (this.ComObject != null)
{
Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
}
//Debug.Assert(false);
}
#endif
public T ComObject
{
get
{
Debug.Assert(!m_disposed);
return m_comObject;
}
}
private string ComObjectName
{
get
{
if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
{
return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
}
return null;
}
}
public void Disarm()
{
Debug.Assert(!m_disposed);
m_armed = false;
}
#region IDisposable Members
public void Dispose()
{
Dispose(true);
#if DEBUG
GC.SuppressFinalize(this);
#endif
}
#endregion
protected virtual void Dispose(bool disposing)
{
if (!m_disposed)
{
if (m_armed)
{
int refcnt = 0;
do
{
refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
} while (refcnt > 0);
m_comObject = default(T);
}
m_disposed = true;
}
}
}
参考:
http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/
首先,在执行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不关闭的原因,即使在读取和创建时为每个对象创建直接引用,也需要使用“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