我该如何在C#应用程序中正确清理Excel interop对象?

3
这个问题已经被问过并得到了很多回答,例如:

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

如何正确处理Interop Excel应用程序和工作簿?

为什么Microsoft.Office.Interop.Excel.Application.Quit()会使后台进程继续运行?

如何处理我的Excel应用程序

但是,Hans Passent对以下问题的回答让我相信它们已经过时和/或不正确:

使用IDisposable清理Excel Interop对象

了解.NET中的垃圾回收

所以,我的问题是: 我如何清理Excel互操作对象,以便在内存压力触发垃圾回收时释放所有托管和非托管的Excel资源?

  • 在发布模式下?
  • 在调试模式下(如果我们关心的话)?

在发布模式下:

仅让所有托管的Excel互操作对象超出范围是否足够?

我需要调用excelApp.Quit()吗?

非托管堆上的内存压力是否会触发垃圾回收?即我是否还需要调用:

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

为了确保我的托管应用程序不会耗尽内存,我需要调用System.Runtime.InteropServices.Marshal.FinalReleaseComObject(managedExcelObject)吗?请注意,除非您已经阅读并理解了Hans Passent的答案,否则请勿回答此问题。

5
大家都知道,曾经与 COM 一起工作的人都理解这些答案,并且可以告诉你相同的内容。在大多数情况下,解决方法是尽可能不使用 Office 互操作性。例如,使用 Epplus 这样的库来创建真正的 xlsx 文件。 - Panagiotis Kanavos
4
我同意。如果没有必要,不要使用交互操作。相反,使用类似于Open XML SDK的东西。请注意,这是一个链接,您可以单击打开了解更多信息。 - Robert Harvey
你是否已经编写了程序并证明这里突出显示的项目是一个问题? - Neil
1
由于我们都有过同样的经历,Interop相关的问题总是比它所值得的更加头疼。如果你只是想将工作表加载到内存中,那么ExcelDataReader也是一个值得考虑的选择。 - Parrish Husband
1
@Robert Harvey 我最初计划用Java编写我的应用程序。但是,这篇文章:https://dev59.com/l1kT5IYBdhLWcg3wNs7- 让我相信Apache POI或OpenXML对图表的支持并不是非常好。因此,我选择了C#和Excel互操作性,因为我相信它会完整且易于使用 - 而且确实如此。我的应用程序是一次性生成一堆(150+)我捕获的csv数据的图表。如果它能完成任务,我并不在意它是否泄漏。这个问题真正的目的是为了扩大我的问题理解。 - d ei
显示剩余5条评论
1个回答

2
随着我对C# Excel互操作的使用变得更加复杂,我发现在关闭应用程序后,在任务管理器中仍有“Microsoft Office Excel (32位)”对象的无头副本运行。我尝试了各种组合的Marshal.ReleaseComObject()和GC.Collect(),但都无法完全消除它们。最终,我删除了所有神秘代码,并遵循了Hans Passent的建议。我能够在大多数情况下使用以下模式终止应用程序关闭时它们的运行:
using System;
using System.IO;
using excel = Microsoft.Office.Interop.Excel;

namespace ExcelInterop {
    static class Program {
        // Create only one instance of excel.Application(). More instances create more Excel objects in Task Manager.
        static excel.Application ExcelApp { get; set; } = new excel.Application();

        [STAThread]
        static int Main() {
            try {
                ExcelRunner excelRunner = new ExcelRunner(ExcelApp)
                // do your Excel interop activities in your excelRunner class here
                // excelRunner MUST be out-of-scope when the finally clause executes
                excelRunner = null;  // not really necessary but kills the only reference to excelRunner 
            } catch (Exception e) {
                // A catch block is required to ensure that the finally block excutes after an unhandled exception
                // see: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/try-finally
                Console.WriteLine($"ExcelRunner terminated with unhandled Exception: '{e.Message}'");
                return -1;
            } finally {
                // this must not execute until all objects derived from 'ExcelApp' are out of scope
                if (ExcelApp != null) {
                    ExcelApp.Quit();
                    ExcelApp = null;
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                }
            }
            Console.WriteLine("ExcelRunner terminated normally");
            return 0;
        }
    }
}

在我的ExcelRunner类中,我将数百个csv文件读入excel工作簿,并创建了几十个具有表格和图表的.xlsx文件。我只创建了一个Microsoft.Office.Interop.Excel.Application()实例,并反复重用它。更多实例意味着在任务管理器中运行需要清理的'Microsoft Office Excel'对象。
请注意,finally子句必须执行以摆脱无头的Excel对象。上述模式处理大多数应用程序关闭情况(包括由未处理异常引起的大多数中止 - 但请参见Does the C# "finally" block ALWAYS execute?)。有一个显着的例外情况,当您从VS调试器中终止应用程序(Shift-F5或工具栏上的红色“停止调试”方块)时。如果您从调试器中终止应用程序,则finally子句不会执行,并且Excel对象会保持运行状态。这很不幸,但我没有找到解决方法。
我在Visual Studio 2019和.NET Framework 4.7.2中使用Excel 2007互操作性和Excel 2016互操作性进行了测试。

非常感谢您提供的代码和解释。有时候我们必须使用互操作性,所以这真的很有帮助。 - undefined
有趣的是,我尝试将您的Main方法重命名,并从不同类中的新Main方法调用它,但在退出后Excel进程仍然存在。 - undefined
@Eric: 我真想不通为什么从另一个方法调用我的方法会有任何区别。你确定你的excelRuner是超出范围并且没有引用吗?虽然已经过了一段时间,但我确信我是从另一个类中调用它的。 - undefined
你问的问题正是我在问的。我曾认为,将调用垃圾回收的方法尽可能靠近程序退出是很重要的。这对我来说有点像巫术。我的测试设置和心态没有我希望的那么敏锐。我将不得不重新审视这个问题。如果我发现任何信息,我会更新这个。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接