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

805

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

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
尽管这样做是有效的,但即使在关闭Excel后,Excel.exe进程仍然在后台运行。只有手动关闭应用程序后,它才会释放。我做错了什么,或者有没有其他方法确保interop对象被正确处理?

3
你是想在不关闭应用程序的情况下关闭Excel.exe吗?我不确定我完全理解你的问题。 - Bryant
4
我正在尝试确保未受控制的互操作对象能够得到正确的处理和释放。这样即使用户已经完成了从应用程序创建的Excel电子表格,也不会出现Excel进程挂起的情况。 - HAdes
4
如果你能尝试通过生成XML Excel文件来完成它,否则请考虑使用VSTO托管/非托管内存管理:http://jake.ginnivan.net/vsto-com-interop - Jeremy Thompson
这个能很好地转换成Excel吗? - Paul C
但是我仍然有两个建议针对上面的代码:1)你应该使用Marshal.FinalReleaseComObject(excelSheet)而不是使用这个while循环2)当“excelSheet”是一个局部变量时,行“excelSheet = null;”是不需要的 - jreichert
2
请参考微软的支持文章,其中他们专门提供了解决此问题的方案:http://support.microsoft.com/kb/317109/ - Arjan
43个回答

711

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

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

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

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

我之前不知道 C# 内部为 Worksheets COM 对象创建了一个包装器,因为我没有意识到这一点,导致 Excel 没有被卸载。

我在这个页面上找到了解决方案,该页面还提供了关于在C#中使用COM对象的好规则:

永远不要在COM对象中使用两个点。


所以,根据这个规则,正确的做法是:

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

事后总结更新:

我希望每个读者都仔细阅读 Hans Passant 的这篇答案,因为它解释了我和许多其他开发人员陷入的陷阱。几年前,当我写下这个答案时,我不知道调试器对垃圾回收器的影响,从而得出了错误的结论。出于历史记录的考虑,我保持我的答案不变,但请阅读此链接并不要走“两个点”的路线:Understanding garbage collection in .NETClean up Excel Interop Objects with IDisposable


18
我建议不要使用COM中的Excel,这样可以避免很多麻烦。你可以在不打开Excel的情况下使用Excel 2007格式,非常方便。 - user7116
6
我不理解“两个点”是什么意思。你能否请解释一下? - A9S6
23
意思是,不应使用模式comObject.Property.PropertiesProperty(您看到了两个点吗?)。相反,将comObject.Property分配给变量,并使用和处理该变量。以上规则的更正式版本可能是:“在使用之前将COM对象分配给变量。这包括另一个COM对象的属性。” - VVS
14
@VSS说的是胡说八道,因为包装变量是由.NET框架创建的,所以垃圾收集器会清理所有东西。只是可能需要很长时间才能清理干净。在进行大量interop操作后调用GC.Collect并不是一个坏主意。 - CodingBarfield
5
这个问题的答案里有错别字吗?还是我漏看了什么?根据这个stackoverflow回答,我们需要按照相反的顺序释放com对象。这是否意味着我们需要在回答中颠倒下面这两行代码的顺序:Marshal.ReleaseComObject(sheets);Marshal.ReleaseComObject(sheet); - user3141326
显示剩余17条评论

286
您可以干净地释放Excel应用程序对象,但需要小心处理。
理论上,为每个访问的COM对象维护一个命名引用并通过Marshal.FinalReleaseComObject()显式释放它的建议是正确的,但不幸的是,在实践中非常难以管理。如果有人在任何地方犯了错误并使用了“两个点”,或通过for each循环迭代单元格,或任何其他类似的命令,则会有未引用的COM对象,并且有悬挂的风险。在这种情况下,没有办法在代码中找到原因;您必须亲自审查所有代码,并希望找到原因,对于大型项目来说这几乎是不可能的任务。
好消息是,您实际上不必为您使用的每个COM对象维护命名变量引用。相反,调用GC.Collect(),然后调用GC.WaitForPendingFinalizers()来释放您没有引用的所有(通常是次要的)对象,然后显式释放您具有命名变量引用的对象。
您还应按重要性的相反顺序释放命名引用:首先是范围对象,然后是工作表、工作簿,最后是您的Excel应用程序对象。
例如,假设您有一个名为xlRng的Range对象变量,一个名为xlSheet的Worksheet变量,一个名为xlBook的Workbook变量和一个名为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#时原则完全相同。

3
可以在ExtremeVBTalk的.NET办公自动化论坛上找到相关讨论,链接为:http://www.xtremevbtalk.com/showthread.php?t=303928。 - Mike Rosenblum
2
如果所有其他方式都失败了,那么可以使用Process.Kill()(作为最后的手段),如此处所述:https://dev59.com/yXVD5IYBdhLWcg3wNYxZ - Mike Rosenblum
1
很高兴它起作用了,Richard。 :-) 这里有一个微妙的例子,仅仅避免“两个点”是不足以防止问题的:https://dev59.com/glTTa4cB1Zd3GeqPqTm5 - Mike Rosenblum
以下是来自微软的Misha Shneerson在2010年10月7日的评论,对于这个话题提出了自己的看法。(http://blogs.msdn.com/b/csharpfaq/archive/2010/09/28/converting-a-vba-macro-to-c-4-0.aspx) - Mike Rosenblum
我希望这可以解决我们一直存在的问题。在finally块中执行垃圾回收和释放调用是否安全? - Brett Green
对我来说起作用了,但只有在将 GC.Collect() 移动到 Marshal.FinalReleaseComObject(xlApp) 之后才行。GC.WaitForPendingFinalizers() 是不必要的。 - primo

230

前言:我的答案包含两种解决方案,请仔细阅读,别漏掉任何东西。

有不同的方法和建议可用于使Excel实例卸载,例如:

  • 使用Marshal.FinalReleaseComObject()显式地释放每个com对象(不要忘记隐式创建的com对象)。为释放每个已创建的com对象,您可以使用此处提到的2点规则:
    如何正确清理Excel互操作对象?

  • 调用GC.Collect()和GC.WaitForPendingFinalizers()来使CLR释放未使用的com对象*(实际上是有效的,请参见我的第二个解决方案以获取详细信息)

  • 检查COM服务器应用程序是否可能显示等待用户回答的消息框(尽管我不确定它是否可以防止Excel关闭,但我听说过几次)

  • 向主Excel窗口发送WM_CLOSE消息

  • 在单独的AppDomain中执行与Excel一起工作的函数。有些人认为卸载AppDomain时,Excel实例将关闭。

  • 杀死所有在我们的Excel-Interop代码启动后实例化的Excel实例。

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

例如,昨天我发现在我的一个函数中(它与Excel一起工作),Excel在函数结束后仍在运行。我尝试了一切!我彻底检查了整个函数10次,并为每个对象添加了Marshal.FinalReleaseComObject()!我也使用了GC.Collect()和GC.WaitForPendingFinalizers()。我检查了隐藏的消息框。我试图向主Excel窗口发送WM_CLOSE消息。我在单独的AppDomain中执行了我的函数并卸载了该域。什么也没用!关闭所有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()无法起作用的最常见原因之一是在Debug模式下运行项目。在Debug模式下,实际上已不再被引用的对象直到方法结束后才被垃圾回收。
所以,如果你尝试了GC.Collect()和GC.WaitForPendingFinalizers(),但没有起作用,请尝试以下操作:

1)尝试在Release模式下运行项目,并检查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将关闭 =)


20
很遗憾,这个帖子已经很老了,所以您的优秀回答被挤到很下面,我认为这是它没有得到更多点赞的唯一原因...... - chiccodoro
16
我必须承认,当我第一次看到你的回答时,我认为它是一个巨大的混乱。经过大约6个小时的努力(一切都已发布,我没有双点等等),我现在认为你的回答非常棒。 - Mark
3
谢谢你。之前我一直在努力关闭一个 Excel 文件,但是无论如何都没有成功,直到我找到了这个方法。太好了。 - BBlake
2
@nightcoder:非常棒的回答,真的很详细。你关于调试模式的评论非常正确和重要,你指出这一点是很有意义的。相同的代码在发布模式下通常可以正常运行。然而,Process.Kill只适用于使用自动化时,而不是当你的程序在Excel中运行时,例如作为托管COM插件。 - Mike Rosenblum
2
@DanM:你的方法是百分之百正确的,永远不会失败。通过将所有变量局限于该方法中,所有引用都无法被.NET代码有效地访问。这意味着当finally部分调用GC.Collect()时,所有 COM对象的终结器都将被确定地调用。然后,每个终结器都会在正在完成的对象上调用Marshal.FinalReleaseComObject。因此,你的方法是简单而可靠的。不要害怕使用它。(唯一的注意事项:如果使用VSTO,我认为你不需要,你需要调用GC.Collect()和GC.WaitForPendingFinalizers两次。) - Mike Rosenblum
显示剩余11条评论

51

更新: 添加了 C# 代码和 Windows Jobs 链接。

我花了一些时间来解决这个问题,当时最活跃和响应最快的是 XtremeVBTalk 论坛。以下是我原始帖子的链接:在应用程序崩溃时清除 Excel Interop 进程。下面是该帖子的摘要和代码副本。

  • 使用 Application.Quit()Process.Kill() 关闭 Interop 进程在大多数情况下有效,但如果应用程序遭遇灾难性崩溃,则会失败。即,如果应用程序崩溃,Excel 进程仍将在运行。
  • 解决方案是通过 Windows Job 对象 使用 Win32 调用,让操作系统处理进程的清理。当主应用程序关闭时,相关进程(即 Excel)也将被终止。

我认为这是一个干净的解决方案,因为操作系统正在做真正的清理工作。你所需要做的就是注册 Excel 进程。

Windows Job 代码

将 Win32 API 调用封装起来以注册 Interop 进程。

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

        int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
        IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
        Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

        if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

构造函数代码的注意事项

  • 在构造函数中,调用了info.LimitFlags = 0x2000;。其中0x2000JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE枚举值,该值由MSDN定义为:

当与作业相关联的最后一个句柄被关闭时,导致所有进程终止。

获取进程ID(PID)的额外Win32 API调用

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

使用代码

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);

3
直接终止一个可能在为其他COM客户端提供服务的外部进程COM服务器是一个可怕的想法。如果你不得不这样做,那是因为你的COM协议出了问题。COM服务器不应该像常规窗口化应用程序一样处理。 - user585968

46

我正在进行的一个项目中使用了这个方法:

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

我们学到了,当你完成使用Excel COM对象时,将每个引用设置为null非常重要。这包括单元格、工作表和所有内容。


41
首先,当进行Excel互操作时,您永远不需要调用Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...)。这是一种令人困惑的反模式,但包括来自Microsoft在内的任何有关手动释放.NET中COM引用的信息都是错误的。事实是,.NET运行时和垃圾回收器可以正确跟踪和清理COM引用。对于您的代码,这意味着您可以删除顶部的整个`while (...)循环。

其次,如果您希望确保在进程结束时清除与外部进程COM对象的COM引用(以便关闭Excel进程),则需要确保垃圾回收器运行。通过调用GC.Collect()GC.WaitForPendingFinalizers()实现正确清理。两次调用是安全的,并确保循环也被清理掉(虽然我不确定是否需要,并且希望有一个示例来说明这一点)。

第三,在调试器下运行时,本地引用将被人为保持活动状态,直到该方法结束(以使本地变量检查工作)。因此,GC.Collect()调用无法有效地清除来自同一方法的rng.Cells等对象。您应将进行COM互操作的代码与GC清理分开成单独的方法。(这是我从@nightcoder在此处发布的答案的一个关键发现。)

因此,一般模式如下:
Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

有关这个问题存在很多虚假信息和困惑,包括 MSDN 和 Stack Overflow 上的许多帖子(特别是这个问题!)。
最后使我确信需要仔细研究并找出正确的建议的是博客文章“Marshal.ReleaseComObject Considered Dangerous”以及发现在调试器下保留引用的问题混淆了我的早期测试。

8
除了这个答案以外,这页上几乎所有的答案都是错误的。它们虽然能够运行,但需要做得过多。请忽略“双点”规则,让垃圾收集器为你完成工作。此外,.Net专家Hans Passant提供了补充证据:https://dev59.com/xV8e5IYBdhLWcg3w_-r0#25135685 - Alan Baljeu
2
Govert,找到正确的方法真是一种解脱。谢谢你。 - Dietrich Baumgarten

30

Excel命名空间中的所有内容都需要释放。无论如何都要释放。

你不能这样做:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

你必须正在做

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

接着释放物体。


3
例如,xlRange.Interior.Color 怎么样? - HAdes
内部需要发布(它位于命名空间中)...另一方面,颜色不需要(因为它来自System.Drawing.Color,如果我没记错的话)。 - MagicKat
2
实际上,Color是Excel中的颜色,而不是.NET中的颜色。你只需要传递一个长整型参数。另外,工作簿(workbooks)确实需要释放,而工作表(worksheets)则相对较少需要释放。 - Anonymous Type
这并不是真的,那个“双点不好、单点好”的规则是无稽之谈。而且,你不需要释放工作簿(Workbooks)、工作表(Worksheets)或范围(Ranges),这是垃圾回收器(GC)的工作。你唯一不能忘记的就是在唯一的Excel.Application对象上调用Quit方法。调用Quit后,将Excel.Application对象置为空值,并调用GC.Collect(); GC.WaitForPendingFinalizers();两次。 - Dietrich Baumgarten

19

找到了一个有用的通用模板,可以帮助实现正确的COM对象清理模式,在其作用域结束时需要调用Marshal.ReleaseComObject:

用法:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

模板:

public class AutoReleaseComObject<T> : IDisposable
{
    private T m_comObject;
    private bool m_armed = true;
    private bool m_disposed = false;

    public AutoReleaseComObject(T comObject)
    {
        Debug.Assert(comObject != null);
        m_comObject = comObject;
    }

#if DEBUG
    ~AutoReleaseComObject()
    {
        // We should have been disposed using Dispose().
        Debug.WriteLine("Finalize being called, should have been disposed");

        if (this.ComObject != null)
        {
            Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
        }

        //Debug.Assert(false);
    }
#endif

    public T ComObject
    {
        get
        {
            Debug.Assert(!m_disposed);
            return m_comObject;
        }
    }

    private string ComObjectName
    {
        get
        {
            if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
            {
                return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
            }

            return null;
        }
    }

    public void Disarm()
    {
        Debug.Assert(!m_disposed);
        m_armed = false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

    #endregion

    protected virtual void Dispose(bool disposing)
    {
        if (!m_disposed)
        {
            if (m_armed)
            {
                int refcnt = 0;
                do
                {
                    refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
                } while (refcnt > 0);

                m_comObject = default(T);
            }

            m_disposed = true;
        }
    }
}

参考资料:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/


3
嗯,这个不错。我认为现在可以更新这段代码,因为FinalReleaseCOMObject已经可用了。 - Anonymous Type
每个对象都需要一个 using 块仍然很烦人。请查看此处 https://dev59.com/qnI95IYBdhLWcg3wsQP9 获取一个替代方案。 - Henrik

18

我简直无法相信这个问题已经困扰了五年… 如果你创建了一个应用程序,在删除链接之前需要先关闭它。

我不敢相信这个问题已经困扰了五年....如果您创建了一个应用程序,需要先关闭它,然后再删除链接。

objExcel = new Excel.Application();  
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing)); 

关闭时

objBook.Close(true, Type.Missing, Type.Missing); 
objExcel.Application.Quit();
objExcel.Quit(); 

在新建Excel应用程序时,它会在后台打开Excel程序。在释放链接之前,您需要命令该Excel程序退出,因为该程序不是您直接控制的一部分。因此,如果释放链接,它将保持打开状态!

祝大家编程愉快~~


不用关闭COM应用程序(在此情况下为Excel),尤其是如果您想允许用户与其进行交互(OP没有指定是否打算进行用户交互,但没有提到关闭它的任何参考)。您只需要让调用者放手,以便用户能够在关闭单个文件时退出Excel。 - downwitch
这对我完美地起作用了 - 当我只有objExcel.Quit()时崩溃了,但是当我事先也执行了objExcel.Application.Quit()时,它干净地关闭了。 - mcmillab
objExcel.Application.Quit()objExcel.Quit() 是相同的调用,因为 objExcel == objExcel.Application。因此,无论您调用哪个,都没有关系。只需要执行一次即可。第二次您再执行(无论是哪个)它已经退出了。 - Alexander Høst

13

这似乎被过度复杂化了。从我的经验来看,使Excel正确关闭只需要三个关键步骤:

1:确保没有剩余引用到你创建的 Excel 应用程序(你应该只有一个; 将其设置为 null

2:调用GC.Collect()

3:Excel 必须关闭,要么是用户手动关闭程序,要么是通过在 Excel 对象上调用 Quit 来关闭(请注意,Quit 的功能就像用户试图关闭程序一样,并且如果存在未保存的更改,则会显示确认对话框,即使 Excel 不可见。用户可以按取消,然后Excel将未被关闭。)

1 需要在 2 之前发生,但是 3 随时都可以发生。

实现这个步骤的一种方法是使用自己的类来包装 interop Excel 对象,在构造函数中创建 interop 实例,并使用 Dispose 实现 IDisposable,Dispose 大致如下:

if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}

这将从程序的角度清除Excel。一旦Excel关闭(由用户手动关闭或通过调用Quit),该进程将消失。如果程序已经关闭,那么在GC.Collect()调用时,该进程将会消失。

(我不确定它有多重要,但可能需要在GC.Collect()调用后加上GC.WaitForPendingFinalizers()调用,但这并不是摆脱Excel进程所必需的。)

这对我而言已经多年无问题地运行了。请记住,虽然这有效,但你实际上必须优雅地关闭才能使其有效。如果在清理Excel之前中断程序(通常是通过在程序调试时按下“停止”按钮),则仍会积累excel.exe进程。


1
这个答案可行,而且比被接受的答案更方便。不用担心使用COM对象时的“两个点”,也不需要使用Marshal - drew.cuthbert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接