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

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

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

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


当前回答

使用Word/Excel互操作应用程序时应该非常小心。在尝试了所有解决方案之后,我们仍然有很多“WinWord”进程在服务器上打开(有2000多个用户)。

在解决这个问题几个小时后,我意识到,如果我在不同的线程上同时使用Word.ApplicationClass.Document.open()打开多个文档,IIS工作进程(w3wp.exe)就会崩溃,所有WinWord进程都会打开!

因此,我想这个问题没有绝对的解决方案,而是改用其他方法,如OfficeOpenXML开发。

其他回答

正如一些人可能已经写过的,如何关闭Excel(对象)不仅重要;如何打开它以及项目类型也很重要。

在WPF应用程序中,基本上相同的代码在没有或很少有问题的情况下工作。

我有一个项目,在该项目中,同一个Excel文件针对不同的参数值被处理了多次-例如,基于通用列表中的值对其进行分析。

我将所有与Excel相关的函数放在基类中,将解析器放在一个子类中(不同的解析器使用通用的Excel函数)。我不希望Excel为泛型列表中的每个项再次打开和关闭,所以我只在基类中打开了一次,并在子类中关闭了它。我在将代码移动到桌面应用程序时遇到了问题。我已经尝试了上面提到的许多解决方案。以前已经实现了GC.Collect(),是建议的两倍。

然后我决定将打开Excel的代码移到一个子类中。现在我不再只打开一次,而是创建一个新对象(基类),为每个项目打开Excel并在最后关闭它。有一些性能损失,但根据几个测试,Excel进程关闭时没有问题(在调试模式下),因此临时文件也会被删除。如果我能得到一些更新,我会继续测试并写更多。

底线是:您还必须检查初始化代码,特别是如果您有许多类等。

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

我猜您调用了COM对象的至少一个成员,而没有将其分配给变量。

对我来说,它是excelApp.Worksheets对象,我直接使用它,而没有将它分配给变量:

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

我不知道C#在内部为WorksheetsCOM对象创建了一个包装器,但我的代码没有发布它(因为我不知道),这也是Excel没有卸载的原因。

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

切勿对COM对象使用两点。


因此,有了这些知识,正确的方法是:

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

尸检后更新:

我希望每一位读者都能非常仔细地阅读Hans Passant的回答,因为它解释了我和许多其他开发人员偶然遇到的陷阱。几年前我写这个答案时,我不知道调试器对垃圾收集器的影响,得出了错误的结论。为了历史起见,我的答案保持不变,但请阅读此链接,不要走“两点”的道路:了解.NET中的垃圾收集和使用IDisposable清理Excel互操作对象

使用Microsoft Excel 2016测试

一个真正经过测试的解决方案。

参考C#,请参见:https://stackoverflow.com/a/1307180/10442623

VB.net参考请参见:https://stackoverflow.com/a/54044646/10442623

1包括课堂作业

2实现该类以处理excel进程的适当处理

我有一个想法,尝试关闭你打开的excel进程:

在打开exclapplication之前,获取名为oldProcessIds的所有进程ID。打开重叠部分。现在获取名为nowProcessIds的所有超应用进程ID。当需要退出时,杀死oldProcessIds和nowProcessIds之间的except id。私有静态Excel.Application GetExcelApp(){如果(_excelApp==空){var processIds=System.Diagnostics.Process.GetProcessesByName(“EXCEL”).Select(a=>a.Id).ToList();_excelApp=新建Excel.Application();_excelApp.DisplayAlerts=false;_excelApp.Visible=false;_excelApp.ScreenUpdate=false;var newProcessIds=System.Diagnostics.Process.GetProcessesByName(“EXCEL”).Select(a=>a.Id).ToList();_excelApplicationProcessId=newProcessIds.Except(processIds).FirstOrDefault();}return _excelApp;}公共静态void Dispose(){尝试{_excelApp.Workbooks.Close();_excelApp.Quit();System.Runtime.InteropServices.Marsal.ReleaseComObject(_excelApp);_excelApp=空;GC.Collect();GC.WaitForPendingFinalizers();if(_excelApplicationProcessId!=默认值(int)){var process=System.Diagnostics.process.GetProcessById(_excelApplicationProcessId);过程Kill();_excelApplicationProcessId=默认值(int);}}catch(异常ex){_excelApp=空;}}

我的回答很晚,其唯一目的是支持戈弗特提出的解决方案。

简短版本:

编写一个没有全局变量和参数的局部函数执行COM内容。在调用COM的包装函数中调用COM函数功能,然后进行清洁。

长版本:

您没有使用.Net来计算COM对象的引用数,并以正确的顺序自行释放它们。即使C++程序员也不再使用智能指针来实现这一点。所以,忘掉Marshal.ReleaseComObject和有趣的一点好两点坏规则吧。如果您对不再需要的COM对象的所有引用都为空,GC很乐意做释放COM对象的工作。最简单的方法是在一个局部函数中处理COM对象,COM对象的所有变量在最后自然地超出了范围。由于Hans Passant的精彩回答中指出了调试器的一些奇怪特性,在Post-Mortem的公认答案中提到,清理应该委托给一个包装函数,该包装函数也调用执行函数。因此,像Excel或Word这样的COM对象需要两个函数,一个执行实际任务,一个包装器调用此函数,然后像Govert那样调用GC,这是本线程中唯一正确的答案。为了说明这个原理,我使用了一个适合所有做COM的函数的包装器。除了这个扩展,我的代码只是Govert代码的C#版本。此外,我停止了该过程6秒,以便您可以在任务管理器中检查Excel在Quit()之后不再可见,而是一直保持僵尸状态,直到GC结束它。

using Excel = Microsoft.Office.Interop.Excel;
public delegate void WrapCom();
namespace GCTestOnOffice{
  class Program{
    static void DoSomethingWithExcel(){
      Excel.Application ExcelApp = new();
      Excel.Workbook Wb = ExcelApp.Workbooks.Open(@"D:\\Sample.xlsx");
      Excel.Worksheet NewWs = Wb.Worksheets.Add();
      for (int i = 1; i < 10; i++){ NewWs.Cells[i, 1] = i;}
      Wb.Save();
      ExcelApp.Quit();
    } 

    static void TheComWrapper(WrapCom wrapCom){
      wrapCom();
      //All COM objects are out of scope, ready for the GC to gobble
      //Excel is no longer visible, but the process is still alive,
      //check out the Task-Manager in the next 6 seconds
      Thread.Sleep(6000);
      GC.Collect();
      GC.WaitForPendingFinalizers();
      GC.Collect();
      GC.WaitForPendingFinalizers();
      //Check out the Task-Manager, the Excel process is gone
    }

    static void Main(string[] args){
      TheComWrapper(DoSomethingWithExcel);
    }
  }
}