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

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

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

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


当前回答

这对我正在进行的一个项目起到了作用:

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

我们了解到,在使用Excel COM对象时,将其每个引用都设置为空非常重要。这包括单元格、工作表和所有内容。

其他回答

这里公认的答案是正确的,但也要注意,不仅需要避免“双点”引用,还需要避免通过索引检索的对象。您也不需要等到程序完成后才能清理这些对象,最好创建函数,以便在可能的情况下,在完成这些对象后立即清理它们。下面是我创建的一个函数,用于分配名为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实例,有不同的方法和建议,例如:

显式释放EVERY com对象使用Marshal.FinalReleaseComObject()(不忘含蓄创建的com对象)。释放每个创建的com对象,您可以使用这里提到的2点规则:如何正确清理Excel互操作对象?调用GC.Collect()和GC.WaitForPendingFinalizers()生成CLR释放未使用的com对象*(实际上,它是有效的,有关详细信息,请参阅我的第二个解决方案)检查com服务器应用程序可能会显示一个等待的消息框用户回答(尽管我不是确保它可以阻止Excel结束了,但我听说过几次次)向主服务器发送WM_CLOSE消息Excel窗口执行有效的函数在单独的AppDomain中使用Excel。有些人相信Excel实例当AppDomain为卸载。终止在excel互操作代码启动后实例化的所有excel实例。

但是!有时所有这些选项都没有帮助或不合适!

例如,昨天我发现在我的一个函数(与excel一起工作)中,excel在函数结束后仍在运行。我什么都试过了!我彻底检查了整个函数10次,并为所有内容添加了Marshal.FinalReleaseComObject()!我还有GC.Collect()和GC.WaitForPendingFinalizers()。我检查了隐藏的消息框。我试图将WM_CLOSE消息发送到Excel主窗口。我在一个单独的AppDomain中执行了我的函数,并卸载了该域。没什么帮助!关闭所有excel实例的选项是不合适的,因为如果用户在执行我的函数(该函数也适用于excel)期间手动启动另一个excel实例,则该实例也将被我的函数关闭。我打赌用户不会高兴的!所以,老实说,这是一个蹩脚的选择(没有冒犯的家伙)。所以我花了几个小时才找到了一个好的(我个人认为)解决方案:通过关闭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将关闭=)

关于释放COM对象的一篇很棒的文章是2.5释放COM对象(MSDN)。

我建议的方法是,如果Excel.Interop引用是非本地变量,则将其置空,然后调用GC.Collect()和GC.WaitForPendingFinalizers()两次。将自动处理本地范围的Interop变量。

这消除了为每个COM对象保留命名引用的需要。

以下是文章中的一个示例:

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

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

这些话直接来自文章:

在几乎所有的情况下,取消RCW引用并强制垃圾收集将正确清理。如果同时调用GC.WaitForPendingFinalizers,垃圾收集将尽可能具有确定性。也就是说,在第二次调用WaitForPending Finalizers返回时,您将非常确定对象何时被清理。作为替代方案,您可以使用Marshal.ReleaseComObject。但是,请注意,您不太可能需要使用此方法。

Excel并非设计为通过C++或C#编程。COM API专门设计用于Visual Basic、VB.NET和VBA。

此外,此页上的所有代码示例都不是最佳的,原因很简单,即每个调用都必须跨越托管/非托管边界,并且还忽略了这样一个事实,即Excel COM API可以自由地使任何调用失败,并带有一个神秘的HRESULT,表示RPC服务器正忙。

我认为自动化Excel的最佳方法是将数据收集到尽可能大的数组中,并将其发送到VBA函数或子函数(通过Application.Run),然后执行任何所需的处理。此外,在调用Application.Run时,请确保注意excel正忙的异常,然后重试调用Application.Run。

这对我正在进行的一个项目起到了作用:

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

我们了解到,在使用Excel COM对象时,将其每个引用都设置为空非常重要。这包括单元格、工作表和所有内容。