我在C#(ApplicationClass)中使用Excel互操作,并在finally子句中放置了以下代码:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。
我做错了什么,或者是否有其他方法可以确保正确处理互操作对象?
为了说明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
正如一些人可能已经写过的,如何关闭Excel(对象)不仅重要;如何打开它以及项目类型也很重要。
在WPF应用程序中,基本上相同的代码在没有或很少有问题的情况下工作。
我有一个项目,在该项目中,同一个Excel文件针对不同的参数值被处理了多次-例如,基于通用列表中的值对其进行分析。
我将所有与Excel相关的函数放在基类中,将解析器放在一个子类中(不同的解析器使用通用的Excel函数)。我不希望Excel为泛型列表中的每个项再次打开和关闭,所以我只在基类中打开了一次,并在子类中关闭了它。我在将代码移动到桌面应用程序时遇到了问题。我已经尝试了上面提到的许多解决方案。以前已经实现了GC.Collect(),是建议的两倍。
然后我决定将打开Excel的代码移到一个子类中。现在我不再只打开一次,而是创建一个新对象(基类),为每个项目打开Excel并在最后关闭它。有一些性能损失,但根据几个测试,Excel进程关闭时没有问题(在调试模式下),因此临时文件也会被删除。如果我能得到一些更新,我会继续测试并写更多。
底线是:您还必须检查初始化代码,特别是如果您有许多类等。
我的回答很晚,其唯一目的是支持戈弗特提出的解决方案。
简短版本:
编写一个没有全局变量和参数的局部函数执行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);
}
}
}
普通开发人员,你的解决方案都不适合我,所以我决定实施一个新的技巧。
首先,让我们指定“我们的目标是什么?”=>“在任务管理器中完成任务后不要看到excel对象”
好的。让no挑战并开始销毁它,但考虑不要销毁并行运行的其他Excel实例。
因此,获取当前处理器的列表并获取EXCEL进程的PID,然后一旦完成任务,我们将在进程列表中创建一个具有唯一PID的新访客,找到并销毁该访客。
<请记住,在excel工作过程中,任何新的excel流程都将被检测为新的并被销毁><更好的解决方案是捕获新创建的excel对象的PID并销毁它>
Process[] prs = Process.GetProcesses();
List<int> excelPID = new List<int>();
foreach (Process p in prs)
if (p.ProcessName == "EXCEL")
excelPID.Add(p.Id);
.... // your job
prs = Process.GetProcesses();
foreach (Process p in prs)
if (p.ProcessName == "EXCEL" && !excelPID.Contains(p.Id))
p.Kill();
这解决了我的问题,也希望你的问题。