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

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

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

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


当前回答

实际上,您可以干净地释放Excel应用程序对象,但您必须小心。

建议为您访问的每个COM对象维护一个命名引用,然后通过Marshal.FinalReleaseComObject()显式释放它,这在理论上是正确的,但不幸的是,在实践中很难管理。如果有人在任何地方滑动并使用“两点”,或者通过for each循环或任何其他类似类型的命令来迭代单元格,那么您将拥有未引用的COM对象并面临挂起的风险。在这种情况下,将无法在代码中找到原因;您必须仔细检查所有代码,并希望找到原因,这对于一个大型项目来说几乎是不可能的。

好消息是,实际上不必维护对所使用的每个COM对象的命名变量引用。相反,先调用GC.Collect(),然后调用GC.WaitForPendingFinalizers(),释放所有未持有引用的对象(通常是次要的),然后显式释放持有命名变量引用的对象。

您还应该按照相反的重要性顺序释放命名引用:首先是范围对象,然后是工作表、工作簿,最后是Excel应用程序对象。

例如,假设您有一个名为xlRng的Range对象变量、一个名名为xlSheet的工作表变量、名为xlBook的工作簿变量和名为xlApp的Excel应用程序变量,则清理代码可能如下所示:

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

在大多数从.NET清理COM对象的代码示例中,GC.Collect()和GC.WaitForPendingFinalizers()调用两次,如下所示:

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

但是,除非您使用的是Visual Studio Tools for Office(VSTO),该工具使用的终结器会导致整个对象图在终结队列中升级,否则这不应该是必需的。在下一次垃圾收集之前,不会释放此类对象。但是,如果您不使用VSTO,则应该能够调用GC.Collect()和GC.WaitForPendingFinalizers()一次。

我知道显式调用GC.Collect()是一个不允许的做法(当然,重复两次听起来很痛苦),但老实说,没有办法解决这个问题。通过正常操作,您将生成隐藏对象,这些对象没有引用,因此,除了调用GC.Collect()之外,您无法通过任何其他方式释放这些对象。

这是一个复杂的主题,但这确实是它的全部内容。一旦为清理过程建立了这个模板,您就可以正常编码,而不需要包装器等:-)

我在这里有一个教程:

用VB.Net/COM Interop实现Office程序的自动化

它是为VB.NET编写的,但不要因此而延迟,其原理与使用C#时完全相同。

其他回答

Excel命名空间中的任何内容都需要发布。时期

你不能这样做:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

你必须这样做

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

随后释放物体。

这看起来确实太复杂了。根据我的经验,要让Excel正确关闭,只有三件关键的事情:

1:确保没有对您创建的excel应用程序的剩余引用(无论如何,您应该只有一个引用;将其设置为空)

2:调用GC.Collect()

3:必须通过用户手动关闭程序或通过对Excel对象调用“退出”来关闭Excel。(请注意,“退出”的功能与用户试图关闭程序的功能相同,如果存在未保存的更改,即使Excel不可见,也会显示一个确认对话框。用户可以按“取消”,Excel将不会关闭。)

1需要在2之前发生,但3可以随时发生。

实现这一点的一种方法是用自己的类包装interop Excel对象,在构造函数中创建interop实例,并使用Dispose实现IDisposable

这将从程序的方面清理出优秀的东西。一旦Excel关闭(由用户手动或您调用退出),该过程将消失。如果程序已经关闭,那么进程将在GC.Collect()调用中消失。

(我不确定它有多重要,但您可能需要在GC.Collect()调用之后调用GC.WaitForPendingFinalizers(),但这并不是完全需要摆脱Excel进程。)

多年来,这对我来说毫无问题。请记住,虽然这是有效的,但实际上您必须优雅地关闭它才能工作。如果在清理excel之前中断程序(通常在调试程序时单击“停止”),则仍会累积excel.exe进程

实际上,您可以干净地释放Excel应用程序对象,但您必须小心。

建议为您访问的每个COM对象维护一个命名引用,然后通过Marshal.FinalReleaseComObject()显式释放它,这在理论上是正确的,但不幸的是,在实践中很难管理。如果有人在任何地方滑动并使用“两点”,或者通过for each循环或任何其他类似类型的命令来迭代单元格,那么您将拥有未引用的COM对象并面临挂起的风险。在这种情况下,将无法在代码中找到原因;您必须仔细检查所有代码,并希望找到原因,这对于一个大型项目来说几乎是不可能的。

好消息是,实际上不必维护对所使用的每个COM对象的命名变量引用。相反,先调用GC.Collect(),然后调用GC.WaitForPendingFinalizers(),释放所有未持有引用的对象(通常是次要的),然后显式释放持有命名变量引用的对象。

您还应该按照相反的重要性顺序释放命名引用:首先是范围对象,然后是工作表、工作簿,最后是Excel应用程序对象。

例如,假设您有一个名为xlRng的Range对象变量、一个名名为xlSheet的工作表变量、名为xlBook的工作簿变量和名为xlApp的Excel应用程序变量,则清理代码可能如下所示:

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

在大多数从.NET清理COM对象的代码示例中,GC.Collect()和GC.WaitForPendingFinalizers()调用两次,如下所示:

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

但是,除非您使用的是Visual Studio Tools for Office(VSTO),该工具使用的终结器会导致整个对象图在终结队列中升级,否则这不应该是必需的。在下一次垃圾收集之前,不会释放此类对象。但是,如果您不使用VSTO,则应该能够调用GC.Collect()和GC.WaitForPendingFinalizers()一次。

我知道显式调用GC.Collect()是一个不允许的做法(当然,重复两次听起来很痛苦),但老实说,没有办法解决这个问题。通过正常操作,您将生成隐藏对象,这些对象没有引用,因此,除了调用GC.Collect()之外,您无法通过任何其他方式释放这些对象。

这是一个复杂的主题,但这确实是它的全部内容。一旦为清理过程建立了这个模板,您就可以正常编码,而不需要包装器等:-)

我在这里有一个教程:

用VB.Net/COM Interop实现Office程序的自动化

它是为VB.NET编写的,但不要因此而延迟,其原理与使用C#时完全相同。

我认为其中一些只是框架处理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对象的一篇很棒的文章是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。但是,请注意,您不太可能需要使用此方法。