我在C#(ApplicationClass)中使用Excel互操作,并在finally子句中放置了以下代码:

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();

尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。

我做错了什么,或者是否有其他方法可以确保正确处理互操作对象?


当前回答

关于释放COM对象的一篇很棒的文章是2.5释放COM对象(MSDN)。

我建议的方法是,如果Excel.Interop引用是非本地变量,则将其置空,然后调用GC.Collect()和GC.WaitForPendingFinalizers()两次。将自动处理本地范围的Interop变量。

这消除了为每个COM对象保留命名引用的需要。

以下是文章中的一个示例:

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

这些话直接来自文章:

在几乎所有的情况下,取消RCW引用并强制垃圾收集将正确清理。如果同时调用GC.WaitForPendingFinalizers,垃圾收集将尽可能具有确定性。也就是说,在第二次调用WaitForPending Finalizers返回时,您将非常确定对象何时被清理。作为替代方案,您可以使用Marshal.ReleaseComObject。但是,请注意,您不太可能需要使用此方法。

其他回答

两点规则不适用于我。在我的案例中,我创建了一种方法来清理我的资源,如下所示:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

在其他答案中考虑的三种一般策略中,杀死excel进程显然是一种黑客行为,而调用垃圾收集器是一种野蛮的猎枪方法,旨在补偿COM对象的错误释放。经过大量的实验,并在我的版本不可知和后期绑定包装器中重写COM对象的管理,我得出结论,准确和及时地调用Marshal.ReleaseComObject()是最有效和优雅的策略。不,您永远不需要FinalReleaseComObject(),因为在一个编写良好的程序中,每个COM只获取一次,因此需要减少一次引用计数器。

应确保释放每个COM对象,最好是在不再需要时释放。但是,在退出Excel应用程序后立即释放所有内容是完全可能的,唯一的代价是更高的内存使用率。只要不松开或忘记释放COM对象,Excel将按预期关闭。

该过程中最简单、最明显的帮助是将每个互操作对象包装到实现IDisposable的.NET类中,其中Dispose()方法在其互操作对象上调用ReleaseComObject()。在析构函数中这样做,正如这里所建议的,没有意义,因为析构函数是非确定性的。

下面显示的是我们的包装器方法,它绕过中间Cells成员从WorkSheet中获取单元格。请注意使用后它处理中间对象的方式:

public ExcelRange XCell( int row, int col)
{   ExcelRange anchor, res;
    using( anchor = Range( "A1") )
    {   res = anchor.Offset( row - 1, col - 1 );  }
    return res;
}

下一步可能是一个简单的内存管理器,它将跟踪获得的每个COM对象,并确保在Excel退出后释放它,如果用户希望用一些RAM来换取更简单的代码。

进一步阅读

如何正确释放Excel COM对象,释放COM对象:垃圾收集器与Marshal.RelseaseComObject。

我认为其中一些只是框架处理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进程,但我可能只是在这方面有点偏执。

这里公认的答案是正确的,但也要注意,不仅需要避免“双点”引用,还需要避免通过索引检索的对象。您也不需要等到程序完成后才能清理这些对象,最好创建函数,以便在可能的情况下,在完成这些对象后立即清理它们。下面是我创建的一个函数,用于分配名为xlStyleHeader的Style对象的一些财产:

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

请注意,我必须将xlBorders[Excel.XlBordersIndex.xlEdgeBottom]设置为一个变量,以清除该变量(不是因为两个点,这两个点指的是不需要释放的枚举,而是因为我所指的对象实际上是一个需要释放的Border对象)。

在标准应用程序中,这种事情并不是真正必要的,因为这些应用程序在清理后会做得很好,但在ASP.NET应用程序中如果您错过了其中的一个,无论您调用垃圾收集器的频率如何,Excel仍将在您的服务器上运行。

编写这段代码时,在监视任务管理器的同时,它需要对细节和许多测试执行进行大量关注,但这样做可以省去在代码页中拼命搜索以查找遗漏的一个实例的麻烦。当在循环中工作时,这一点尤其重要,因为您需要释放对象的每个实例,即使它每次循环都使用相同的变量名。

为了说明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