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


747

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

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

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

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


1
您是否要在不关闭应用程序的情况下关闭Excel.exe?不确定我是否完全理解您的问题。
科比

3
我正在尝试确保正确处理非托管互操作对象。这样,即使用户完成了从应用程序创建的Excel电子表格,也不会出现任何Excel流程徘徊。
哈德斯

3
如果您可以尝试通过生成XML Excel文件来做到这一点,否则请考虑使用VSTO 不受
Jeremy Thompson,

这可以很好地转换为Excel吗?
合作社2012年

2
请参阅(下面的答案除外)来自Microsoft的此支持文章,他们在其中专门为该问题提供解决方案:support.microsoft.com/kb/317109
Arjan

Answers:


684

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的答案,因为它解释了我和许多其他开发人员所陷入的陷阱。几年前,当我写这个答案时,我不知道调试器对垃圾收集器的影响并得出错误的结论。为了历史起见,我的答案保持不变,但是请阅读此链接,不要采用“两个点”的方式:了解.NET中的垃圾收集使用IDisposable清理Excel Interop对象


16
然后,我建议不要使用COM中的Excel,从而省去了所有麻烦。无需打开Excel,就可以使用Excel 2007格式。
user7116

5
我不明白什么是“两个点”。你能解释一下吗?
A9S6 2010年

22
这意味着,您不应使用模式comObject.Property.PropertiesProperty(您看到两个点吗?)。而是将comObject.Property分配给变量,然后使用和处置该变量。可能是上述规则的更正式版本。如“在使用变量之前将com对象分配给变量。这包括作为另一个com对象属性的com对象。”
VVS 2010年

5
@Nick:实际上,您不需要任何清理,因为垃圾收集器会为您完成清理。您唯一需要做的就是将每个COM对象分配给它自己的变量,以便GC知道它。
VVS

12
@VSS多数民众赞成在锁,GC清理一切,因为包装变量是由.net框架。GC可能要永久清除它。在重互操作后调用GC.Collect并不是一个坏主意。
CodingBarfield 2011年

280

实际上,您可以干净地释放Excel Application对象,但是必须小心。

对于绝对访问的每个COM对象都保留一个命名引用,然后通过显式释放它的建议Marshal.FinalReleaseComObject()在理论上是正确的,但是不幸的是,在实践中很难管理。如果一个人滑到任何地方并使用“两个点”,或者通过for each循环或其他任何类似的命令来迭代单元,那么您将拥有未引用的COM对象,并可能会死机。在这种情况下,将无法在代码中找到原因。您将不得不仔细检查所有代码并希望找到原因,这对于大型项目而言几乎是不可能的。

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

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

例如,假设您有一个名为Range的对象变量xlRng,一个名为Worksheet的变量xlSheet,一个名为Workbook的变量xlBook和名为的Excel Application变量xlApp,那么您的清理代码可能类似于以下内容:

// 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()调用TWICE进行了如下操作:

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

但是,除非您使用的是Visual Studio Tools for Office(VSTO),否则它不是必需的,因为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 Office Automation论坛上找到相关讨论,网址为:xtremevbtalk.com/showthread.php?t=303928
Mike Rosenblum

2
如果一切都失败了,那么Process.Kill()都可以使用(作为最后手段)如下所述:stackoverflow.com/questions/51462/...
麦克布鲁姆

1
理查德很高兴。:-)这里是一个微妙的例子,其中简单地避免了“两个点”并不足以防止一个问题:stackoverflow.com/questions/4964663/...
麦克布鲁姆

微软的Misha Shneerson在2010年10月7日的评论日期中对此发表了意见。(blogs.msdn.com/b/csharpfaq/archive/2010/09/28/…
Mike Rosenblum

我希望这可以解决我们遇到的一个持久性问题。在finally块中进行垃圾回收和释放调用是否安全?
布雷特·格林

213

前言:我的答案包含两种解决方案,因此阅读时请务必小心,不要错过任何内容。

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

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

  • 调用GC.Collect()和GC.WaitForPendingFinalizers()可使CLR释放未使用的COM对象*(实际上,它可以工作,有关详细信息,请参见我的第二个解决方案)

  • 检查com-server-application是否显示一个等待用户回答的消息框(尽管我不确定它是否可以阻止Excel关闭,但我听说过几次)

  • 将WM_CLOSE消息发送到Excel主窗口

  • 在单独的AppDomain中执行与Excel一起使用的功能。有人认为,卸载AppDomain后,Excel实例将关闭。

  • 杀死所有在我们的excel互操作代码启动后实例化的excel实例。

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

例如,昨天我发现在我的其中一个函数(与excel一起使用)中,Excel在函数结束后一直运行。我尝试了一切!我彻底检查了整个功能10次,并为所有内容添加了Marshal.FinalReleaseComObject()!我也有GC.Collect()和GC.WaitForPendingFinalizers()。我检查了隐藏的消息框。我试图将WM_CLOSE消息发送到Excel主窗口。我在单独的AppDomain中执行了我的功能,然后卸载了该域。没有任何帮助!关闭所有excel实例的选项是不合适的,因为如果用户在我的函数执行期间手动启动另一个Excel实例(也可以使用Excel),则该实例也将被我的函数关闭。我敢打赌用户不会满意!因此,说实话,这是一个la脚的选择(没有冒犯的人)。解决方案通过其主窗口的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模式,我提供了两种方法(我认为这里很合适):如果无法杀死Process(例如,该进程不再存在),则一种方法不会引发异常。 ,如果Process没有被杀死,则另一个方法将引发异常。此代码中唯一的弱项是安全权限。从理论上讲,用户可能没有杀死进程的权限,但是在所有情况下,有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模式下运行项目。在调试模式下,不再真正引用的对象将在方法结束之前不会被垃圾回收。
因此,如果您尝试使用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将关闭=)


19
令人遗憾的是,该线程已经太旧了,您的最佳答案就出现在下面,我认为这是它没有被多次投票的唯一原因……
chiccodoro 2010年

15
我不得不承认,当我初读你的答案时,我认为这是一个巨大的冲突。经过大约6个小时的努力(所有内容都发布了,我没有双点了,等等。),我现在认为您的答案是天才。
标记

3
谢谢你 在我发现这个问题之前的几天里,他们一直在努力使用一个不会关闭的Excel。优秀的。
BBlake

2
@nightcoder:很棒的答案,真的很详细。您指出的有关调试模式的评论非常真实和重要。在发布模式下使用相同的代码通常可以。但是,Process.Kill仅在使用自动化时才有用,而当您的程序在Excel中运行时(例如,作为托管COM加载项)则无济于事。
Mike Rosenblum

2
@ DanM:您的方法是100%正确的,并且永远不会失败。通过将所有变量都保留在方法的本地,.NET代码实际上无法访问所有引用。这意味着,当您的finally节调用GC.Collect()时,将确定地调用所有 COM对象的终结器。然后,每个终结器在要终结的对象上调用Marshal.FinalReleaseComObject。因此,您的方法既简单又简单。不用担心使用它。(仅需注意:如果使用VSTO,我怀疑您是这样,则需要调用GC.Collect()和GC.WaitForPendingFinalizers TWICE。)
Mike Rosenblum

49

更新:添加了C#代码,并链接到Windows作业

我花了一些时间试图找出这个问题,当时XtremeVBTalk是最活跃和响应最快的。这是我原始文章的链接,即使您的应用程序崩溃了,也要彻底关闭Excel Interop进程。下面是该帖子的摘要,以及将代码复制到了该帖子中。

  • 使用Application.Quit()和关闭Interop进程Process.Kill()在大多数情况下都有效,但是如果应用程序崩溃严重,则失败。即,如果应用程序崩溃,则Excel进程仍将无法正常运行。
  • 解决方案是让操作系统使用Win32调用通过Windows Job Objects处理进程的清理。当主应用程序死亡时,关联的进程(即Excel)也将终止。

我发现这是一个干净的解决方案,因为操作系统正在做清理工作。您要做的就是注册 Excel流程。

Windows作业代码

包装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定义为:

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

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

    [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);

2
明确杀死一个进程外的COM服务器(可能正在为其他COM客户端提供服务)是一个可怕的想法。如果您要这样做,是因为您的COM协议已损坏。COM服务器不应被视为常规的窗口应用程序
MickyD 2015年

38

这适用于我正在从事的项目:

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

我们了解到,完成对Excel COM对象的所有引用后,将其设置为null 至关重要。其中包括单元格,表格和其他所有内容。


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。
哈德斯

内部需要释放(在名称空间中)...另一方面,颜色不需要(由于System.Drawing.Color,iirc引起的)
MagicKat

2
实际上,颜色是Excel颜色,而不是.Net颜色。你只是通过一个龙。还工作簿定义。需要发布,工作表...更少。
匿名类型

那是不对的,两点坏,一点好规则是胡说八道。另外,您不必发布工作簿,工作表,范围,这是GC的工作。您永远不会忘记的唯一事情是在一个唯一的Excel.Application对象上调用Quit。调用Quit之后,将Excel.Application对象设置为空,然后调用GC.Collect();。GC.WaitForPendingFinalizers(); 两次。
Dietrich Baumgarten

29

首先-你永远不会有打电话Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...)做的Excel互操作时。这是一个令人困惑的反模式,但是有关此的任何信息(包括来自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被认为是危险的,同时发现在调试器下保存的引用仍然存在问题,这使我的早期测试感到困惑。


5
实际上,除此页面外,该页面上的所有答案均不正确。他们工作,但是工作太多。忽略“ 2 DOTS”规则。让GC为您完成工作。来自.Net GURU Hans Passant的补充证据:stackoverflow.com/a/25135685/3852958
Alan Baljeu

1
Govert,找到正确的方法实在是一件轻松的事。谢谢。
Dietrich Baumgarten

18

找到了一个有用的通用模板,该模板可以帮助实现COM对象的正确处理模式,这些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更新此代码。
匿名类型

每个对象都有一个using块仍然很烦人。请参阅此处stackoverflow.com/questions/2191489/…以获取替代方法。
Henrik 2012年

16

我不能相信这个问题困扰了全世界5年。...如果创建了一个应用程序,则需要先关闭它,然后再删除链接。

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程序退出,因为该excel程序不是您直接控制的一部分。因此,如果释放链接,它将保持打开状态!

大家好编程~~


不,您不需要,尤其是如果您希望允许用户与COM应用程序进行交互(在这种情况下,Excel是OP,它没有指定是否要进行用户交互,但没有提及将其关闭)。您只需要让调用者放手,用户就可以在关闭单个文件时退出Excel。
Downwitch'Mar

这对我来说非常有效-当我只有objExcel.Quit()时崩溃,但是当我也事先拥有objExcel.Application.Quit()时,崩溃了。
mcmillab

13

普通开发人员,您的解决方案都不适合我,因此我决定实施一个新技巧

首先让我们指定“我们的目标是什么?” =>“在任务管理器中工作后看不到excel对象”

好。让no挑战并开始破坏它,但考虑不要破坏并行运行的其他实例os 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();

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


1
....这有点像雪橇战法吗?-类似于我正在使用的::(但需要更改。使用此方法的问题是,通常在打开excel时,在运行该程序的计算机上,它的左手边有警报,像这样“Excel中被错误地关闭了”:。不是很优雅,我认为在这个线程早前建议之一是首选
whytheq

11

这种确定似乎过于复杂了。根据我的经验,只有以下三个关键事项才能使Excel正确关闭:

1:确保没有剩余对您创建的excel应用程序的引用(无论如何,您都应该只有一个引用;将其设置为null

2:致电 GC.Collect()

3:必须通过用户手动关闭程序或通过调用QuitExcel对象来关闭Excel。(请注意,该Quit功能的作用就如同用户尝试关闭该程序一样,并且即使未显示Excel,也存在未保存的更改,将显示一个确认对话框。用户可以按“取消”,然后将不会关闭Excel。 )

1必须在2之前发生,但3可以随时发生。

一种实现方法是用您自己的类包装互操作Excel对象,在构造函数中创建互操作实例,并使用Dispose实现IDisposable,类似于

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

这将从程序方面清除excel。Excel关闭后(手动由用户或您致电Quit),该过程将消失。如果程序已经关闭,则该过程将在GC.Collect()调用中消失。

(我不确定它的重要性如何,但是您可能想在GC.WaitForPendingFinalizers()通话后再GC.Collect()通话,但严格来说,摆脱Excel流程并不是必须的。)

多年来,这对我一直没有问题。请记住,尽管这样做有效,但实际上您必须优雅地关闭它才能正常工作。如果在清理Excel之前中断程序(通常是在调试程序时按“停止”),您仍然会积累excel.exe进程。


1
该答案有效,并且比接受的答案更方便。不用担心COM对象的“两个点”,无需使用Marshal
andrew.cuthbert

9

补充说明为什么即使在读取,创建后对每个对象创建直接引用时,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

另一个原因是,重用引用而不先释放先前的值。
Grimfort 2010年

谢谢。我还使用“针对每个”的解决方案为我工作。
乍得·布劳恩·杜因

+1谢谢。另外,请注意,如果您有一个用于Excel范围的对象,并且想要在对象的生命周期内更改范围,我发现在重新分配它之前我必须先使用ReleaseComObject,这会使代码有些混乱!
AjV Jsy

9

传统上,我遵循VVS答案中的建议。但是,为了使此答案与最新选项保持最新,我认为我以后的所有项目都将使用“ NetOffice”库。

NetOffice完全替代了Office PIA,并且与版本完全无关。它是托管COM包装程序的集合,可以处理在.NET中使用Microsoft Office时经常引起此类麻烦的清理操作。

一些主要功能是:

  • 主要与版本无关(并记录了与版本有关的功能)
  • 没有依赖
  • 没有PIA
  • 没有注册
  • 没有VSTO

我绝不隶属于该项目;我真的很感谢头痛的明显减轻。


2
确实,这应该被标记为答案。NetOffice消除了所有这些复杂性。
C. Augusto Proiete 2015年

1
我一直在使用NetOffice来编写Excel加载项,并且运行良好。唯一要考虑的是,如果不显式处理使用过的对象,则在退出应用程序时会这样做(因为无论如何它都会跟踪它们)。因此,与NetOffice经验法则是始终使用“使用”模式与每一个Excel对象样细胞,范围或片材等
斯塔斯诺夫

8

此处接受的答案是正确的,但也请注意,不仅需要避免“两个点”的引用,而且还需要避免通过索引检索的对象。您也不必等到完成程序清理这些对象后,最好创建函数,尽可能在完成后清理它们。这是我创建的一个函数,用于分配称为的Style对象的一些属性xlStyleHeader

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都会仍在您的服务器上运行。

在编写此代码时,在监视“任务管理器”时,它需要大量关注细节和许多测试执行,但是这样做省却了您拼命地搜索代码页面以查找丢失的一个实例的麻烦。这在循环中工作时特别重要,在循环中您需要释放对象的每个实例,即使对象每次循环都使用相同的变量名。


在Release内部,检查Null(Joe的答案)。这样可以避免不必要的null异常。我已经测试了很多方法。这是有效发布Excel的唯一方法。
Gerhard Powell

8

经过尝试

  1. 以相反的顺序释放COM对象
  2. GC.Collect(),最后GC.WaitForPendingFinalizers()两次
  3. 不超过两个点
  4. 关闭工作簿并退出应用程序
  5. 在发布模式下运行

对我有用的最终解决方案是移动一组

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

我们在函数的末尾添加了一个包装器,如下所示:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

我发现我不需要使ComObjects无效,只需退出Quit()-Close()和FinalReleaseComObject。我无法相信这就是我要使它正常工作所缺少的全部。大!
卡罗尔

7

我完全按照这个步骤进行...但是我仍然遇到1000次问题中的1次。谁知道为什么。是时候拿出锤子了...

在实例化Excel Application类之后,我立即掌握了刚刚创建的Excel进程。

excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();

然后,完成上述所有COM清理后,请确保该进程未运行。如果它仍在运行,请杀死它!

if (!process.HasExited)
   process.Kill();

7

¸射击Excel proc并咀嚼泡泡糖¸„ø¤º°¨

public class MyExcelInteropClass
{
    Excel.Application xlApp;
    Excel.Workbook xlBook;

    public void dothingswithExcel() 
    {
        try { /* Do stuff manipulating cells sheets and workbooks ... */ }
        catch {}
        finally {KillExcelProcess(xlApp);}
    }

    static void KillExcelProcess(Excel.Application xlApp)
    {
        if (xlApp != null)
        {
            int excelProcessId = 0;
            GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
            Process p = Process.GetProcessById(excelProcessId);
            p.Kill();
            xlApp = null;
        }
    }

    [DllImport("user32.dll")]
    static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}

6

您需要注意,Excel也对您所运行的文化非常敏感。

您可能会发现需要在调用Excel函数之前将区域性设置为EN-US。这不适用于所有功能-但其中一些功能。

    CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); 
    System.Threading.Thread.CurrentThread.CurrentCulture = en_US;
    string filePathLocal = _applicationObject.ActiveWorkbook.Path;
    System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;

即使您使用的是VSTO,这也适用。

有关详细信息:http : //support.microsoft.com/default.aspx?scid=kb;zh-cn;Q320369


6

根据经验,“切勿在COM对象上使用两个点”是避免泄漏COM引用的一个很好的经验法则,但是Excel PIA可能导致泄漏的方式比乍看时要明显得多。

这些方法之一是订阅由Excel对象模型的COM对象公开的任何事件。

例如,订阅Application类的WorkbookOpen事件。

关于COM事件的一些理论

COM类通过回调接口公开一组事件。为了订阅事件,客户端代码可以简单地注册一个实现回调接口的对象,并且COM类将响应特定事件来调用其方法。由于回调接口是COM接口,因此实现对象有责任为任何事件处理程序减少它接收到的任何COM对象(作为参数)的引用计数。

Excel PIA如何公开COM事件

Excel PIA将Excel Application类的COM事件公开为常规.NET事件。只要客户端代码预订了.NET事件(强调“ a”),PIA就会创建实现回调接口的类实例,并将其注册到Excel。

因此,响应于.NET代码的不同订阅请求,许多回调对象已在Excel中注册。每个事件订阅一个回调对象。

用于事件处理的回调接口意味着,PIA必须为每个.NET事件订阅请求订阅所有接口事件。它不能选择。接收到事件回调后,回调对象将检查关联的.NET事件处理程序是否对当前事件感兴趣,然后调用该处理程序或静默忽略该回调。

对COM实例引用计数的影响

所有这些回调对象都不会减少任何回调方法(甚至对于那些被静默忽略的对象)收到的任何COM对象(作为参数)的引用计数。它们仅依靠CLR垃圾收集器释放COM对象。

由于GC运行是不确定的,因此这可能导致Excel进程推迟的时间比预期的更长,并给人以“内存泄漏”的印象。

到目前为止,唯一的解决方案是避免为COM类使用PIA的事件提供程序,而编写自己的事件提供程序,以确定性地释放COM对象。

对于Application类,可以通过实现AppEvents接口,然后使用IConnectionPointContainer接口在Excel中注册实现来完成。Application类(以及所有使用回调机制公开事件的COM对象)实现IConnectionPointContainer接口。


4

正如其他人指出的那样,您需要为使用的每个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对象。

但是,这种方法不能很好地满足更复杂的需求。

这是.NET COM Interop的一大缺陷。对于更复杂的方案,我将认真考虑使用VB6或其他不受管理的语言编写ActiveX DLL,您可以将与Office这类出站COM对象的所有交互委派给该ActiveX DLL。然后,您可以从.NET应用程序中引用此ActiveX DLL,这将使事情变得容易得多,因为您只需要释放该引用即可。


3

如果以上所有内容均无效,请尝试给Excel一些时间以关闭其工作表:

app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);

2
我讨厌等待。但是+1是因为您需要等待工作簿关闭是正确的。另一种选择是在循环中轮询工作簿集合,并使用任意等待作为循环超时。
dF11 2011年

3

确保释放所有与Excel相关的对象!

我花了几个小时尝试几种方法。都是不错的主意,但我终于发现了自己的错误:如果您不释放所有对象,那么上述任何一种方法都无法帮助您。确保释放所有对象,包括范围一!

Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);

选项在这里在一起。


很好点!我认为,因为我使用的是Office 2007,所以我需要进行一些清理。我已经进一步使用了advice,但是没有按照您在此处建议的那样存储变量,EXCEL.EXE确实退出了,但是我很幸运,如果我有任何其他问题,我肯定会看一下我的代码的这一部分=)
Coops

3

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

我会提倡的方法是,如果他们非本地变量设置为null您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,则垃圾回收将尽可能地具有确定性。也就是说,您将确切地确定何时清理了对象-从第二次调用WaitForPendingFinalizers返回时。或者,您可以使用Marshal.ReleaseComObject。但是,请注意,您极不可能需要使用此方法。


2

两点规则对我不起作用。就我而言,我创建了一种清理资源的方法,如下所示:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

2

我的解决方案

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

private void GenerateExcel()
{
    var excel = new Microsoft.Office.Interop.Excel.Application();
    int id;
    // Find the Excel Process Id (ath the end, you kill him
    GetWindowThreadProcessId(excel.Hwnd, out id);
    Process excelProcess = Process.GetProcessById(id);

try
{
    // Your code
}
finally
{
    excel.Quit();

    // Kill him !
    excelProcess.Kill();
}

2

使用Word / Excel互操作应用程序时应格外小心。在尝试了所有解决方案之后,我们仍然在服务器(拥有2000多个用户)上打开了许多“ WinWord”进程。

在解决了几个小时的问题后,我意识到,如果Word.ApplicationClass.Document.Open()同时在不同的线程上使用多个文档,则IIS工作进程(w3wp.exe)将会崩溃,所有WinWord进程都处于打开状态!

因此,我想没有绝对的解决方案来解决这个问题,而是切换到其他方法,例如Office Open XML开发。


1

接受的答案对我不起作用。析构函数中的以下代码完成了这项工作。

if (xlApp != null)
{
    xlApp.Workbooks.Close();
    xlApp.Quit();
}

System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach (System.Diagnostics.Process process in processArray)
{
    if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}


1

我目前正在从事Office自动化方面的工作,偶然发现了一个对我来说每次都有效的解决方案。它很简单,不涉及杀死任何进程。

似乎仅通过循环当前活动的进程,并以任何“访问”开放的Excel进程的方式,就可以删除任何散乱的Excel实例。以下代码仅检查名称为“ Excel”的进程,然后将进程的MainWindowTitle属性写入字符串。与进程的这种“交互”似乎使Windows赶上并中止了冻结的Excel实例。

我正在开发的外接程序退出之前运行以下方法,因为它触发了它的卸载事件。每次都删除任何悬挂的Excel实例。老实说,我不完全确定为什么会这样,但是它对我来说很好,可以放在任何Excel应用程序的末尾,而不必担心双点,Marshal.ReleaseComObject或终止进程。我对任何为什么有效的建议都非常感兴趣。

public static void SweepExcelProcesses()
{           
            if (Process.GetProcessesByName("EXCEL").Length != 0)
            {
                Process[] processes = Process.GetProcesses();
                foreach (Process process in processes)
                {
                    if (process.ProcessName.ToString() == "excel")
                    {                           
                        string title = process.MainWindowTitle;
                    }
                }
            }
}

1

我认为其中某些只是框架处理Office应用程序的方式,但我可能是错的。有时,某些应用程序会立即清理进程,而另一些日子似乎要等到应用程序关闭。通常,我退出时会注意细节,只是要确保一天结束时没有其他多余的流程。

另外,也许我正在简化事情,但是我认为您可以...

objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();

就像我之前说的,我不倾向于关注Excel进程何时出现或消失的细节,但这通常对我有用。除了最短的时间之外,我也不想保留Excel进程,但我可能对此感到偏执。


1

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

在WPF应用程序中,基本上相同的代码可以正常工作,而不会出现或很少有问题。

我有一个项目,其中相同的Excel文件针对不同的参数值进行了多次处理-例如,根据通用列表中的值进行解析。

我将所有与Excel相关的函数放入基类,并将解析器放入子类(不同的解析器使用常见的Excel函数)。我不希望再次为通用列表中的每个项目打开和关闭Excel,因此我只在基类中打开了一次,然后在子类中将其关闭了。将代码移至桌面应用程序时遇到问题。我已经尝试了许多上述解决方案。GC.Collect()之前已经实施过,是建议的两倍。

然后,我决定将将打开Excel的代码移至子类。现在,我创建一个新对象(基类),而不是只打开一次,然后为每个项目打开Excel并在最后将其关闭。有一些性能损失,但是基于多个测试,Excel进程正在关闭,没有问题(在调试模式下),因此还删除了临时文件。我将继续测试,并在获得一些更新的情况下编写更多内容。

最重要的是:您还必须检查初始化代码,尤其是如果您有很多类,等等。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.