如何正确清理Excel互操作对象?

我在C#中使用Excel interop( ApplicationClass ),并在我的finally子句中放置了下面的代码:

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

虽然这种工作, Excel.exe进程仍然在后台,即使我关闭了Excel。 只有在我的应用程序被手动关闭时才会被释放。

我做错了什么,或者有没有其他方法可以确保互操作对象被妥善处置?


Excel不会退出,因为您的应用程序仍然保存对COM对象的引用。

我想你至少要调用一个COM对象的成员而不将其分配给一个变量。

对我来说,这是我直接使用的excelApp.Worksheets对象,而不用将它分配给一个变量:

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);

我不知道内部C#为工作表 COM对象创建了一个包装,它没有被我的代码发布(因为我没有意识到),并且是Excel未被卸载的原因。

我在这个页面上找到了解决我的问题的解决方案,这对于C#中COM对象的使用也有一个很好的规则:

切勿在COM对象中使用两个点。


所以有了这些知识,正确的做法是:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

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

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

好消息是,您实际上不必为所使用的每个COM对象维护一个命名变量引用。 相反,调用GC.Collect() ,然后GC.WaitForPendingFinalizers()以释放所有不包含引用的(通常较小的)对象,然后明确释放您持有指定变量引用的对象。

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

例如,假设你有一个名为Range对象变量xlRng ,命名工作表变量xlSheet ,命名工作簿变量xlBook并命名为Excel应用程序变量xlApp ,那么你的清理代码可能类似于以下内容:

// 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()调用是在TWICE中进行的,如下所示:

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

但是,这不应该是必需的,除非您使用Visual Studio Tools for Office(VSTO),该工具使用终结器,以便在完成队列中提升整个对象图。 这些对象直到下一次垃圾收集才会被释放。 但是,如果您不使用VSTO,则应该只能调用一次GC.Collect()GC.WaitForPendingFinalizers()

我知道明确调用GC.Collect()是一个GC.Collect()当然,这两次听起来非常痛苦),但说实话,没有办法绕过它。 通过正常的操作,您将生成隐藏的对象,因此除了调用GC.Collect()之外,您不能通过任何其他方式释放引用。

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

我在这里有一个教程:

使用VB.Net / COM Interop自动执行Office程序

它是为VB.NET编写的,但不要被推迟,原则与使用C#时完全相同。


前言:我的答案包含两个解决方案,所以在阅读时要小心,不要错过任何东西。

关于如何使Excel实例卸载有不同的方式和建议,例如:

  • 使用Marshal.FinalReleaseComObject()显式释放EVERY com对象(不要忘记隐式创建的com对象)。 要释放每个创建的COM对象,您可以使用这里提到的2个点的规则:
    如何正确清理Excel互操作对象?

  • 调用GC.Collect()和GC.WaitForPendingFinalizers()来使CLR释放未使用的com对象*(实际上,它可以工作,请参阅我的第二个解决方案以了解详细信息)

  • 检查com-server-application是否显示一个消息框,等待用户回答(虽然我不确定它可以防止Excel关闭,但我几次听说过)

  • 发送WM_CLOSE消息到主Excel窗口

  • 在单独的AppDomain中执行与Excel一起工作的函数。 有些人认为,当AppDomain被卸载时,Excel实例将被关闭。

  • 杀死所有在我们的excel-interoping代码启动后实例化的excel实例。

  • 但! 有时候,所有这些选项都无济于事或不适合!

    例如,昨天我发现在我的一个函数中(它可以与excel一起工作),Excel在函数结束后继续运行。 我尝试了一切! 我彻底地检查了整个函数10次,并添加了Marshal.FinalReleaseComObject()来做所有事情! 我也有GC.Collect()和GC.WaitForPendingFinalizers()。 我检查了隐藏的消息框。 我试图将WM_CLOSE消息发送到主Excel窗口。 我在一个单独的AppDomain中执行我的函数并卸载了该域。 没什么帮助! 关闭所有excel实例的选项是不合适的,因为如果用户手动启动另一个Excel实例,在执行与Excel一起工作的函数期间,那么该实例也将被我的函数关闭。 我敢打赌,用户不会高兴! 所以,说实话,这是一个蹩脚的选择(没有进攻球员)。 所以我花了几个小时才找到一个好的(以我愚蠢的观点) 解决方案通过主窗口的hWnd杀死excel进程 (这是第一种解决方案)。

    这是简单的代码:

    [DllImport("user32.dll")]
    private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
    
    /// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
    /// <param name="hWnd">Handle to the main window of the process.</param>
    /// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
    public static bool TryKillProcessByMainWindowHwnd(int hWnd)
    {
        uint processID;
        GetWindowThreadProcessId((IntPtr)hWnd, out processID);
        if(processID == 0) return false;
        try
        {
            Process.GetProcessById((int)processID).Kill();
        }
        catch (ArgumentException)
        {
            return false;
        }
        catch (Win32Exception)
        {
            return false;
        }
        catch (NotSupportedException)
        {
            return false;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
        return true;
    }
    
    /// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
    /// <param name="hWnd">Handle to the main window of the process.</param>
    /// <exception cref="ArgumentException">
    /// Thrown when process is not found by the hWnd parameter (the process is not running). 
    /// The identifier of the process might be expired.
    /// </exception>
    /// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
    /// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
    /// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
    public static void KillProcessByMainWindowHwnd(int hWnd)
    {
        uint processID;
        GetWindowThreadProcessId((IntPtr)hWnd, out processID);
        if (processID == 0)
            throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
        Process.GetProcessById((int)processID).Kill();
    }
    

    正如你所看到的,我根据Try-Parse模式提供了两种方法(我认为这是适当的):如果进程无法被杀死(例如进程不再存在),则一种方法不会抛出异常,另一种方法如果进程未被终止则抛出异常。 此代码中唯一的弱点是安全权限。 理论上,用户可能没有权限来终止进程,但在99.99%的情况下,用户拥有这样的权限。 我还用一个访客帐户对它进行了测试 - 它完美地工作。

    因此,使用Excel的代码看起来像这样:

    int hWnd = xl.Application.Hwnd;
    // ...
    // here we try to close Excel as usual, with xl.Quit(),
    // Marshal.FinalReleaseComObject(xl) and so on
    // ...
    TryKillProcessByMainWindowHwnd(hWnd);
    

    瞧! Excel终止! :)

    好的,让我们回到第二个解决方案,正如我在文章开头所承诺的那样。 第二种解决方案是调用GC.Collect()和GC.WaitForPendingFinalizers()。 是的,他们实际上工作,但你需要小心!
    许多人说(我说)调用GC.Collect()并没有帮助。 但它没有帮助的原因是,如果仍然有COM对象的引用! GC.Collect()没有帮助的最常见原因之一是以调试模式运行项目。 在调试模式下,不再真正引用的对象将不会被垃圾收集,直到方法结束。
    因此,如果您尝试使用GC.Collect()和GC.WaitForPendingFinalizers(),但它没有帮助,请尝试执行以下操作:

    1)尝试在发布模式下运行你的项目,并检查Excel是否正确关闭

    2)用独立的方法将使用Excel的方法包装起来。 所以,而不是像这样的东西:

    void GenerateWorkbook(...)
    {
      ApplicationClass xl;
      Workbook xlWB;
      try
      {
        xl = ...
        xlWB = xl.Workbooks.Add(...);
        ...
      }
      finally
      {
        ...
        Marshal.ReleaseComObject(xlWB)
        ...
        GC.Collect();
        GC.WaitForPendingFinalizers();
      }
    }
    

    你写:

    void GenerateWorkbook(...)
    {
      try
      {
        GenerateWorkbookInternal(...);
      }
      finally
      {
        GC.Collect();
        GC.WaitForPendingFinalizers();
      }
    }
    
    private void GenerateWorkbookInternal(...)
    {
      ApplicationClass xl;
      Workbook xlWB;
      try
      {
        xl = ...
        xlWB = xl.Workbooks.Add(...);
        ...
      }
      finally
      {
        ...
        Marshal.ReleaseComObject(xlWB)
        ...
      }
    }
    

    现在,Excel将关闭=)

    链接地址: http://www.djcxy.com/p/6059.html

    上一篇: How do I properly clean up Excel interop objects?

    下一篇: How to use Mathematica functions in Python programs?