首页 > C# > 如何正确清理 Excel 互操作对象?

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

上一篇 下一篇

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

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

尽管这种工作,但即使在我关闭Excel之后,该过程仍在后台进行。它仅在我的应用程序手动关闭后才会释放。Excel.exe

我做错了什么,或者是否有其他方法可以确保正确处置互操作对象?
分割线
网友回答:

您实际上可以干净地释放 Excel 应用程序对象,但您必须小心。

从理论上讲,为您访问的每个 COM 对象维护命名引用然后显式释放它的建议是正确的,但不幸的是,在实践中很难管理。如果有人滑到任何地方并使用“两个点”,或者通过循环或任何其他类似类型的命令迭代单元格,那么您将拥有未引用的 COM 对象并面临挂起的风险。在这种情况下,将无法在代码中找到原因;你必须通过眼睛检查所有代码,并希望找到原因,这对于一个大型项目来说几乎是不可能的。Marshal.FinalReleaseComObject()for each

好消息是,您实际上不必维护对您使用的每个 COM 对象的命名变量引用。相反,调用 然后释放您不持有引用的所有(通常是次要的)对象,然后显式释放您持有命名变量引用的对象。GC.Collect()GC.WaitForPendingFinalizers()

您还应该按重要性的相反顺序释放命名引用:首先是范围对象,然后是工作表、工作簿,最后是 Excel 应用程序对象。

例如,假设您有一个名为 的范围对象变量 ,一个名为 的工作表变量 、一个名为工作簿变量和一个名为 的 Excel 应用程序变量,那么您的清理代码可能如下所示:xlRngxlSheetxlBookxlApp

// 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 互操作自动执行办公程序

它是为 VB.NET 编写的,但不要因此而推迟,其原理与使用 C# 时完全相同。
分割线
网友回答:

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

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

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

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

我不知道内部 C# 为工作表 COM 对象创建了一个包装器,我的代码没有发布该包装器(因为我不知道),这是 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 互操作对象
分割线
网友回答:

前言:我的回答包含两种解决方案,所以阅读时要小心,不要错过任何东西。

对于如何卸载 Excel 实例,有不同的方法和建议,例如:

  • 使用 Marshal.FinalReleaseComObject()
    显式
    释放每个 com 对象(不要忘记隐
    式创建的 com-objects)。要释放
    每个创建的 com 对象,您可以使用
    此处提到的 2 个点的规则:
    如何正确清理 Excel 互操作对象?
  • 调用 GC。Collect() 和
    GC。WaitForPendingFinalizers() 使
    CLR 释放未使用的 com-objects * (实际上,它有效,有关详细信息,请参阅我的第二个解决方案)
  • 检查com-server-application
    是否可能显示一个消息框,等待用户回答(虽然我不确定
    它可以阻止Excel
    关闭,
    但我听说过几次
  • 将WM_CLOSE消息发送到 Excel 主
    窗口
  • 在单独的应用程序域中执行与 Excel 配合使用的
    函数。
    有些人认为,当卸载 AppDomain
    时,Excel 实例
    将被关闭。
  • 终止在我们的 excel 互操作代码启动后实例化的所有 excel 实例。

但!有时所有这些选项都无济于事或不适合!

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

模板简介:该模板名称为【如何正确清理 Excel 互操作对象?】,大小是暂无信息,文档格式为.编程语言,推荐使用Sublime/Dreamweaver/HBuilder打开,作品中的图片,文字等数据均可修改,图片请在作品中选中图片替换即可,文字修改直接点击文字修改即可,您也可以新增或修改作品中的内容,该模板来自用户分享,如有侵权行为请联系网站客服处理。欢迎来懒人模板【C#】栏目查找您需要的精美模板。

相关搜索
  • 下载密码 lanrenmb
  • 下载次数 293次
  • 使用软件 Sublime/Dreamweaver/HBuilder
  • 文件格式 编程语言
  • 文件大小 暂无信息
  • 上传时间 03-12
  • 作者 网友投稿
  • 肖像权 人物画像及字体仅供参考
栏目分类 更多 >
热门推荐 更多 >
html5 自适应 单页式简历模板 微信文章 企业网站 微信模板 微信图片 微信素材 微信公众平台 响应式
您可能会喜欢的其他模板