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

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

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

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


当前回答

这里公认的答案是正确的,但也要注意,不仅需要避免“双点”引用,还需要避免通过索引检索的对象。您也不需要等到程序完成后才能清理这些对象,最好创建函数,以便在可能的情况下,在完成这些对象后立即清理它们。下面是我创建的一个函数,用于分配名为xlStyleHeader的Style对象的一些财产:

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

请注意,我必须将xlBorders[Excel.XlBordersIndex.xlEdgeBottom]设置为一个变量,以清除该变量(不是因为两个点,这两个点指的是不需要释放的枚举,而是因为我所指的对象实际上是一个需要释放的Border对象)。

在标准应用程序中,这种事情并不是真正必要的,因为这些应用程序在清理后会做得很好,但在ASP.NET应用程序中如果您错过了其中的一个,无论您调用垃圾收集器的频率如何,Excel仍将在您的服务器上运行。

编写这段代码时,在监视任务管理器的同时,它需要对细节和许多测试执行进行大量关注,但这样做可以省去在代码页中拼命搜索以查找遗漏的一个实例的麻烦。当在循环中工作时,这一点尤其重要,因为您需要释放对象的每个实例,即使它每次循环都使用相同的变量名。

其他回答

实际上,您可以干净地释放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对象创建一个显式引用,并对该引用调用Marshal.ReleaseComObject,如本知识库文章所述。您还需要使用try/finally来确保始终调用ReleaseComObject,即使抛出异常也是如此。即,代替:

Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet

你需要做一些事情,比如:

Worksheets sheets = null;
Worksheet sheet = null
try
{ 
    sheets = excelApp.Worksheets;
    sheet = sheets(1);
    ...
}
finally
{
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    if (sheet != null) Marshal.ReleaseComObject(sheet);
}

如果要关闭Excel,还需要在释放Application对象之前调用Application.Quit。

正如您所看到的,只要您尝试做任何稍微复杂的事情,这很快就会变得非常笨拙。我用一个简单的包装类成功地开发了.NET应用程序,该类包装了Excel对象模型的一些简单操作(打开工作簿、写入范围、保存/关闭工作簿等)。包装器类实现IDisposable,在它使用的每个对象上仔细地实现Marshal.ReleaseComObject,并且不向应用程序的其他部分公开任何Excel对象。

但这种方法不能很好地适应更复杂的需求。

这是.NETCOM互操作的一大缺陷。对于更复杂的场景,我会认真考虑用VB6或其他非托管语言编写ActiveX DLL,您可以将与进程外COM对象(如Office)的所有交互委托给它。然后,您可以从.NET应用程序中引用此ActiveX DLL,因为您只需要发布这一个引用,所以事情会变得更加简单。

Use:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

声明它,在finally块中添加代码:

finally
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (excelApp != null)
    {
        excelApp.Quit();
        int hWnd = excelApp.Application.Hwnd;
        uint processID;
        GetWindowThreadProcessId((IntPtr)hWnd, out processID);
        Process[] procs = Process.GetProcessesByName("EXCEL");
        foreach (Process p in procs)
        {
            if (p.Id == processID)
                p.Kill();
        }
        Marshal.FinalReleaseComObject(excelApp);
    }
}

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

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

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

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

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

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

普通开发人员,你的解决方案都不适合我,所以我决定实施一个新的技巧。

首先,让我们指定“我们的目标是什么?”=>“在任务管理器中完成任务后不要看到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();

这解决了我的问题,也希望你的问题。