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

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

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

虽然这种工作,Excel.exe进程仍然在后台,即使我关闭Excel。只有当我的应用程序手动关闭时,它才会被释放。

我做错了什么,或者是否有替代方案来确保互操作对象被正确处理?

339949 次浏览

这适用于我正在进行的一个项目:

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

我们了解到,在完成对Excel COM对象的引用后将其设置为null很重要。这包括单元格、工作表和所有内容。

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

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

对我来说,它是我直接使用的ExcelApp工作表相关文档对象,而没有将其分配给变量:

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

我不知道C#在内部为工作表 COM对象创建了一个包装器,我的代码没有释放它(因为我不知道它),这是Excel没有卸载的原因。

我在此页面上找到了我的问题的解决方案,它对C#中COM对象的使用也有一个很好的规则:

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


所以有了这些知识,做上述事情的正确方法是:

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

死后更新:

我希望每个读者都能非常仔细地阅读Hans Passant的这个答案,因为它解释了我和许多其他开发人员偶然陷入的陷阱。几年前我写这个答案时,我不知道调试器对垃圾收集器的影响,并得出了错误的结论。为了历史的缘故,我保持我的答案不变,但请阅读这个链接,不要走“两个点”的路:. NET中的垃圾回收机制使用IDisposable清理Excel互操作对象

Excel命名空间中的任何内容都需要释放。句号

你不能这样做:

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

你必须做

Workbooks books = excel.WorkBooks;Workbook book = books[1];Sheets sheets = book.WorkSheets;Worksheet ws = sheets[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进程,但我可能只是偏执。

正如其他人所指出的,您需要为您使用的每个Excel对象创建一个显式引用,并在该引用上调用Marshall. ReleaseComObject,如这篇KB文章中所述。您还需要使用try/最终来确保始终调用ReleaseComObject,即使抛出异常。即:

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

你需要做这样的事情:

Worksheets sheets = null;Worksheet sheet = nulltry{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对象模型的一些简单操作(打开工作簿、写入Range、保存/关闭工作簿等)。包装类实现了IDisposable,在它使用的每个对象上仔细实现了Marshall. ReleaseComObject,并且不会公开向应用程序的其余部分公开任何Excel对象。

但是这种方法不能很好地扩展到更复杂的需求。

这是. NET COM互操作的一个很大的缺陷。对于更复杂的场景,我会认真考虑用VB6或其他非托管语言编写一个ActiveX DLL,您可以将与out-proc COM对象(如Office)的所有交互委托给它。然后,您可以从您的. NET应用程序引用此ActiveX DLL,事情会容易得多,因为您只需要释放此引用。

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

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

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

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

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

// CleanupGC.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互操作自动化Office程序

它是为VB.NET编写的,但不要因此而推迟,原则与使用C#时完全相同。

您需要知道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;en-us;Q320369

I发现一个有用的泛型模板,可以帮助实现COM对象的正确处置模式,当它们超出范围时需要调用Marshall. 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 DEBUGGC.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/

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

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

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

我发现这是一个干净的解决方案,因为操作系统正在做真正的清理工作。你所要做的就是寄存器 Excel进程。

Windows作业代码

包装Win32 API调用以注册互操作进程。

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

普通开发人员,你们的解决方案都不适合我,所以我决定实现一个新的把戏

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

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

前言:我的回答包含两种解法,所以阅读时一定要小心,不要漏掉任何东西。

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

  • 显式释放每个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次,并为所有内容添加了Marshall. FinalReleaseComObject()!我还有GC. Collect()和GC. WaitForPendingFinalizers()。我检查了隐藏的消息框。我尝试将WM_CLOSE消息发送到主Excel窗口。我在一个单独的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();}

正如你所看到的,我提供了两个方法,根据Trit-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将关闭=)

这里接受的答案是正确的,但也要注意,不仅需要避免“两点”引用,还需要避免通过索引检索的对象。你也不需要等到程序完成后才清理这些对象,最好创建一些函数,在可能的情况下,这些函数会在你完成后立即清理它们。这是我创建的一个函数,它分配了一个名为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 FormatxlStyleHeader.NumberFormat = "@";
// BoldxlFont = xlStyleHeader.Font;xlFont.Bold = true;
// Light Gray Cell ColorxlInterior = xlStyleHeader.Interior;xlInterior.Color = 12632256;
// Medium Bottom borderxlBorders = 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/317109try{System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);}catch { }}

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

这种事情在标准应用程序中并不是真正必要的,它们可以很好地清理自己,但是在ASP.NET应用程序中,如果您错过了其中的一个,无论您多久调用一次垃圾收集器,Excel仍然会在您的服务器上运行。

在编写此代码时,在监控任务管理器时需要非常关注细节和多次测试执行,但这样做可以节省您拼命搜索代码页面以找到您错过的一个实例的麻烦。这在循环中工作时尤其重要,在循环中,您需要释放对象的每个实例,即使它每次循环都使用相同的变量名称。

我不敢相信这个问题已经困扰了世界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程序不是您直接控制的一部分。因此,如果链接被释放,它将保持打开状态!

大家编程不错~~

为了增加Excel不关闭的原因,即使您在读取时创建对每个对象的直接引用,创建也是“For”循环。

For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dotobjWorkBook.Close 'or whateverFinalReleaseComObject(objWorkBook)objWorkBook = NothingNext
'The above does not work, and this is the workaround:
For intCounter As Integer = 1 To mobjExcel_WorkBooks.CountDim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)objTempWorkBook.Saved = TrueobjTempWorkBook.Close(False, Type.Missing, Type.Missing)FinalReleaseComObject(objTempWorkBook)objTempWorkBook = NothingNext

当上面所有的东西都不起作用时,试着给Excel一些时间来关闭它的工作表:

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

Excel不是为通过C++或C#编程而设计的。COM API是专门为使用Visual Basic、VB.NET和VBA而设计的。

此外,此页面上的所有代码示例都不是最佳的,原因很简单,即每个调用都必须跨越托管/非托管边界,并且进一步忽略了Excel COM API可以自由地使任何调用失败,其中包含指示RPC服务器忙的神秘HRESULT

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

这确实看起来过于复杂了。根据我的经验,只有三件关键事情可以让Excel正确关闭:

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

2:调用GC.Collect()

3: Excel必须关闭,要么由用户手动关闭程序,要么由您在Excel对象上调用Quit。(请注意,Quit的功能与用户尝试关闭程序相同,如果有未保存的更改,即使Excel不可见,也会显示确认对话框。用户可以按取消,然后Excel不会关闭。)

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

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

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

这将从您的程序方面清理excel。一旦Excel关闭(由用户手动或您调用Quit),该进程将消失。如果程序已经关闭,那么该进程将在GC.Collect()调用时消失。

(我不确定它有多重要,但您可能希望在GC.Collect()调用之后进行GC.WaitForPendingFinalizers()调用,但摆脱Excel进程并不是绝对必要的。

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

您应该非常小心使用Word/Excel互操作应用程序。在尝试了所有解决方案之后,我们仍然有很多“WinWord”进程在服务器上开放(拥有超过2000个用户)。

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

所以我猜这个问题没有绝对的解决方案,而是切换到其他方法,比如Office Open XML开发。

“永远不要对COM对象使用两个点”是一个很好的经验法则,可以避免COM引用的泄漏,但Excel PIA可能会导致泄漏的方式比乍看起来要多。

其中一种方法是订阅任何Excel对象模型的COM对象公开的任何事件。

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

关于COM事件的一些理论

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

Excel PIA如何公开COM事件

Excel PIA将Excel Application类的COM事件公开为常规的。NET事件。每当客户端代码订阅a. net事件(强调'a')时,PIA都会创建实现回调接口的类的一个实例并将其注册到Excel。

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

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

对COM实例引用计数的影响

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

由于GC运行是不确定的,这可能导致Excel进程的延迟时间超过预期,并造成“内存泄漏”的印象。

解决方案

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

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

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

我花了几个小时尝试了几种方法。所有这些都是很棒的想法,但我终于发现了我的错误:如果你不释放所有对象,上面的方法都帮不了你就像我的情况一样。确保释放所有对象,包括范围1!

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

这些选项在一起这里

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

我提倡的方法是,如果Excel. Interop引用是非局部变量,则将其空,然后调用GC.Collect()GC.WaitForPendingFinalizers()两次。本地范围的互操作变量将自动处理。

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

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

public class Test {
// These instance variables must be nulled or Excel will not quitprivate 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的返回时。作为替代方案,您可以使用Marshall. ReleaseComObject。但是,请注意,您不太可能需要使用此方法。

我传统上遵循VVS的答案中的建议。然而,为了使这个答案与最新的选项保持同步,我认为我未来的所有项目都将使用“NetOffice”库。

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

一些主要特点是:

  • 大部分与版本无关(并且记录了与版本相关的功能)
  • 无依赖关系
  • 没有PIA
  • 没有登记
  • 没有VSTO

我与这个项目没有任何关系;我只是真诚地欣赏头痛的明显减少。

正如有些人可能已经写过的那样,重要的不仅仅是你如何关闭 Excel(对象);同样重要的是你如何开放它以及项目的类型。

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

我有一个项目,其中相同的Excel文件被多次处理以获取不同的参数值-例如,根据通用列表中的值解析它。

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

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

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

¨°º¤ø„¸ 拍摄Excel过程和咀嚼泡泡糖 ¸„ø¤º°¨

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

用途:

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

在尝试

  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();}}

两点规则对我不起作用。在我的例子中,我创建了一个方法来清理我的资源,如下所示:

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

我完全按照这个……但我还是遇到了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();

我的解决方案

[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 himGetWindowThreadProcessId(excel.Hwnd, out id);Process excelProcess = Process.GetProcessById(id);
try{// Your code}finally{excel.Quit();
// Kill him !excelProcess.Kill();}

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

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. 终止进程
  2. 使用GC. Collect()
  3. 跟踪每个COM对象并正确释放它。

这让我意识到这个问题有多困难:)

我一直在开发一个库来简化对Excel的访问,我正在努力确保使用它的人不会留下混乱(手指交叉)。

我没有直接在Interop提供的接口上编写,而是制作了扩展方法来让生活更轻松。就像Application ationHelpers. CreateExcel()或workbook. CreateWorktable("mySheetNameThatWillBeVal的")。自然,创建的任何内容都可能导致以后清理时出现问题,所以我实际上倾向于作为最后的手段终止进程。然而,正确清理(第三种选择)可能是破坏性最小、受控最多的。

所以,在这种情况下,我想知道是否最好做这样的事情:

public abstract class ReleaseContainer<T>{private readonly Action<T> actionOnT;
protected ReleaseContainer(T releasible, Action<T> actionOnT){this.actionOnT = actionOnT;this.Releasible = releasible;}
~ReleaseContainer(){Release();}
public T Releasible { get; private set; }
private void Release(){actionOnT(Releasible);Releasible = default(T);}}

我使用“Releaable”来避免与Disposable混淆。不过,将其扩展到IDisposable应该很容易。

像这样的实现:

public class ApplicationContainer : ReleaseContainer<Application>{public ApplicationContainer(): base(new Application(), ActionOnExcel){}
private static void ActionOnExcel(Application application){application.Show(); // extension method. want to make sure the app is visible.application.Quit();Marshal.FinalReleaseComObject(application);}}

可以对所有类型的COM对象执行类似的操作。

在工厂方法中:

    public static Application CreateExcelApplication(bool hidden = false){var excel = new ApplicationContainer().Releasible;excel.Visible = !hidden;
return excel;}

我希望每个容器都将被GC正确销毁,因此会自动调用QuitMarshal.FinalReleaseComObject

评论?或者这是对第三类问题的回答?

只是为了在这里列出的许多解决方案中添加另一个解决方案,使用C++ /ATL自动化(我想你可以使用类似于VB/C#的东西??)

Excel::_ApplicationPtr pXL = ...:SendMessage ( ( HWND ) m_pXL->GetHwnd ( ), WM_DESTROY, 0, 0 ) ;

这对我来说就像一个魅力…

首先,在执行Excel互操作时,您从未必须调用Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...)。这是一个令人困惑的反模式,但任何有关此的信息,包括来自Microsoft的信息,都表明您必须手动释放来自. NET的COM引用是不正确的。事实是. NET运行时和垃圾收集器正确地跟踪和清理COM引用。对于您的代码,这意味着您可以删除顶部的整个'同时(…)循环。

其次,如果您想确保在进程结束时清理对进程外COM对象的COM引用(以便Excel进程将关闭),您需要确保垃圾收集器运行。您可以正确调用GC.Collect()GC.WaitForPendingFinalizers()。调用两次是安全的,并确保循环也绝对被清理(尽管我不确定是否需要,并且希望有一个示例显示这一点)。

第三,在调试器下运行时,本地引用将被人为地保持活动状态,直到方法结束(以便本地变量检查起作用)。因此GC.Collect()调用对于从同一方法中清理像rng.Cells这样的对象无效。您应该将执行COM互操作的代码从GC清理拆分为单独的方法。(这对我来说是一个关键的发现,来自@night coder在这里发布的答案的一部分。)

因此,一般的模式是:

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 ExcelDoTheWork()
' 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.ApplicationDim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")app.Visible = TrueFor i As Integer = 1 To 10worksheet.Cells.Range("A" & i).Value = "Hello"Nextbook.Save()book.Close()app.Quit()
' NOTE: No calls the Marshal.ReleaseComObject() are ever neededEnd Sub

关于这个问题有很多错误的信息和混乱,包括MSDN和Stack Overflow上的许多帖子(尤其是这个问题!)。

最终说服我仔细研究并找出正确的建议的是博客文章Marshal. ReleaseComObject被认为是危险的,以及在调试器下发现引用保持活跃的问题,这使我之前的测试感到困惑。

我目前正在研究办公自动化,并偶然发现了一个解决方案,每次对我都有效。它很简单,不涉及杀死任何进程。

似乎仅仅通过循环当前活动进程,并以任何方式“访问”打开的Excel进程,任何流浪的挂起Excel实例都将被删除。下面的代码只是检查名称为“Excel”的进程,然后将进程的MainWindowtitle属性写入字符串。与进程的这种“交互”似乎使Windows赶上并中止冻结的Excel实例。

我在我正在开发的加载项退出之前运行以下方法,因为它触发了它的卸载事件。它每次都会删除Excel的任何挂起实例。老实说,我不完全确定为什么这有效,但它对我很有效,可以放置在任何Excel应用程序的末尾,而不必担心双点、Marshall. 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;}}}}

这是唯一对我有效的方法

        foreach (Process proc in System.Diagnostics.Process.GetProcessesByName("EXCEL")){proc.Kill();}

这确实看起来过于复杂了。根据我的经验,让Excel正确关闭只有三件关键事情:

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

2:调用GC. Collect()

3: Excel必须关闭,用户可以手动关闭程序,或者在Excel对象上调用Quit。(请注意,Quit的功能与用户尝试关闭程序时相同,如果有未保存的更改,即使Excel不可见,也会显示确认对话框。用户可以按取消,然后Excel将不会关闭。)

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

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

这将从您的程序方面清理excel。一旦Excel关闭(由用户手动或您调用Quit),该进程将消失。如果程序已经关闭,则该进程将在GC. Collect()调用中消失。

(我不确定它有多重要,但您可能希望在GC. Collect()调用之后调用GC. WaitForPendingFinalizers(),但并不一定需要摆脱Excel进程。)

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

我有一个想法,尝试杀死你打开的Excel进程:

  1. 在打开excelApplication之前,获取所有名为oldProcessIds的进程ID。
  2. 打开ExcelApplication。
  3. 现在获取名为now ProcessIds的所有excelApplication进程ID。
  4. 当需要退出时,在oldProcessIds和now ProcessIds之间杀死除id。

    private static Excel.Application GetExcelApp(){if (_excelApp == null){var processIds = System.Diagnostics.Process.GetProcessesByName("EXCEL").Select(a => a.Id).ToList();_excelApp = new Excel.Application();_excelApp.DisplayAlerts = false;
    _excelApp.Visible = false;_excelApp.ScreenUpdating = false;var newProcessIds = System.Diagnostics.Process.GetProcessesByName("EXCEL").Select(a => a.Id).ToList();_excelApplicationProcessId = newProcessIds.Except(processIds).FirstOrDefault();}
    return _excelApp;}
    public static void Dispose(){try{_excelApp.Workbooks.Close();_excelApp.Quit();System.Runtime.InteropServices.Marshal.ReleaseComObject(_excelApp);_excelApp = null;GC.Collect();GC.WaitForPendingFinalizers();if (_excelApplicationProcessId != default(int)){var process = System.Diagnostics.Process.GetProcessById(_excelApplicationProcessId);process?.Kill();_excelApplicationProcessId = default(int);}}catch (Exception ex){_excelApp = null;}
    }

通过Microsoft Excel 2016测试

一个真正经过测试的解决方案。

到C#参考请参阅:https://stackoverflow.com/a/1307180/10442623

VB.net参考:https://stackoverflow.com/a/54044646/10442623

1包括班级作业

2实现类来处理Excel过程的适当处理

这里有一个非常简单的方法来做到这一点:

[DllImport("User32.dll")]static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);...
int objExcelProcessId = 0;
Excel.Application objExcel = new Excel.Application();
GetWindowThreadProcessId(new IntPtr(objExcel.Hwnd), out objExcelProcessId);
Process.GetProcessById(objExcelProcessId).Kill();

在我的VSTO AddIn中更新Application对象后,我在关闭PowerPoint时遇到了同样的问题。我尝试了这里的所有答案,但收效甚微。

这是我为我的案例找到的解决方案-DONT使用“new Application”,ThisAddIn的AddInBase基类已经有一个“Application”的句柄。如果您在需要它的地方使用该句柄(如果必须的话,将其设置为静态),那么您无需担心清理它,PowerPoint也不会挂起。

我的回答是迟到的,它的唯一目的是支持戈弗特提出的解决方案。

简短版本:

  • 编写一个没有全局变量和参数的局部函数执行COM的东西。

  • 在调用COM的包装函数中调用COM函数功能和清洁之后。

长版本:

您没有使用. Net来计算COM对象的引用并以正确的顺序释放它们。即使C++程序员也不再使用智能指针来这样做。所以,忘掉Marshal.ReleaseComObject和有趣的一点好两点坏规则吧。如果您清空所有不再需要的COM对象的引用,GC很乐意做释放COM对象的苦差事。最简单的方法是在局部函数中处理COM对象,COM对象的所有变量最终都会自然超出范围。由于一些奇怪的调试器功能,在Hans Passant的精彩回答中指出,清理应该委托给一个也调用执行函数的包装函数。因此,像Excel或Word这样的COM对象需要两个函数,一个完成实际工作,一个调用这个函数并随后调用GC的包装器,就像Goversion一样,这是这个线程中唯一正确的答案。为了展示原则,我使用了一个适用于所有做COM工作的函数的包装器。除了这个扩展,我的代码只是Goversion代码的C#版本。此外,我停止了该进程6秒,以便您可以在任务管理器中检查Excel在Quit()之后不再可见,而是作为僵尸继续存在,直到GC结束它。

using Excel = Microsoft.Office.Interop.Excel;public delegate void WrapCom();namespace GCTestOnOffice{class Program{static void DoSomethingWithExcel(){Excel.Application ExcelApp = new();Excel.Workbook Wb = ExcelApp.Workbooks.Open(@"D:\\Sample.xlsx");Excel.Worksheet NewWs = Wb.Worksheets.Add();for (int i = 1; i < 10; i++){ NewWs.Cells[i, 1] = i;}Wb.Save();ExcelApp.Quit();}
static void TheComWrapper(WrapCom wrapCom){wrapCom();//All COM objects are out of scope, ready for the GC to gobble//Excel is no longer visible, but the process is still alive,//check out the Task-Manager in the next 6 secondsThread.Sleep(6000);GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();GC.WaitForPendingFinalizers();//Check out the Task-Manager, the Excel process is gone}
static void Main(string[] args){TheComWrapper(DoSomethingWithExcel);}}}

在其他答案中考虑的三种一般策略中,杀死excel进程显然是一种黑客攻击,而调用垃圾收集器是一种残酷的猎枪方法,旨在补偿COM对象的错误释放。在我的版本无关和后期绑定包装器中,经过大量的实验和重写COM对象的管理,我得出的结论是,准确及时地调用Marshal.ReleaseComObject()是最有效和优雅的策略。不,你永远不需要FinalReleaseComObject(),因为在编写良好的程序中,每个COM一次获得,因此需要减少一次引用计数器。

应该确保释放每个COM对象,最好是在不再需要它时立即释放。但是完全有可能在退出Excel应用程序后立即释放所有内容,唯一的代价是更高的内存占用。只要不丢失或忘记释放COM对象,Excel将按预期关闭。

该过程中最简单和最明显的帮助是将每个互操作对象包装到一个实现IDisposable. NET类中,其中Dispose()方法在其互操作对象上调用ReleaseComObject()。在析构函数中这样做,如这里中提议的,没有意义,因为析构函数是非确定性的。

下面显示的是我们的包装器方法,它从WorkSheet绕过中间Cells成员获得一个单元格。请注意它在使用后处置中间对象的方式:

public ExcelRange XCell( int row, int col){   ExcelRange anchor, res;using( anchor = Range( "A1") ){   res = anchor.Offset( row - 1, col - 1 );  }return res;}

下一步可能是一个简单的内存管理器,它将跟踪获得的每个COM对象,并确保在Excel退出后释放它,如果用户更愿意用一些RAM使用来换取更简单的代码。

进一步阅读

  1. 如何正确释放Excel COM对象
  2. 释放COM对象:垃圾收集器与元帅. RelseeComObject

我真的很喜欢事情自己清理干净……所以我做了一些包装类,它们为我做了所有的清理工作!这些记录在后面。

结束代码非常可读和可访问。我还没有发现任何在我Close()工作簿和Quit()应用程序之后运行的Excel幻像实例(除了我调试和关闭应用程序中间进程的地方)。

function void OpenCopyClose() {var excel = new ExcelApplication();var workbook1 = excel.OpenWorkbook("C:\Temp\file1.xslx", readOnly: true);var readOnlysheet = workbook1.Worksheet("sheet1");
var workbook2 = excel.OpenWorkbook("C:\Temp\file2.xslx");var writeSheet = workbook.Worksheet("sheet1");
// do all the excel manipulation
// read from the first workbook, write to the second workbook.var a1 = workbook1.Cells[1, 1];workbook2.Cells[1, 1] = a1
// explicit clean-upworkbook1.Close(false);workbook2 .Close(true);excel.Quit();}

注意:您可以跳过Close()Quit()调用,但如果您正在写入Excel文档,您至少需要Save()。当对象超出作用域(方法返回)时,类终结器将自动启动并进行任何清理。只要您小心处理变量的作用域,就会自动管理和清理工作表COM对象中对COM对象的任何引用,例如仅在存储对COM对象的引用时将变量保留在当前作用域的本地。如果需要,您可以轻松地将需要的值复制到POCO,或者创建如下所述的附加包装类。

为了管理所有这些,我创建了一个类DisposableComObject,它充当任何COM对象的包装器。它实现了IDisposable接口,还包含一个不喜欢using的终结器。

Dispose()方法调用Marshal.ReleaseComObject(ComObject),然后将ComObjectRef属性设置为null。

当私有ComObjectRef属性为空时,对象处于已释放状态。

如果ComObject属性在被释放后被访问,则会引发ComObjectAccessedAfterDisposeException异常。

Dispose()方法可以手动调用。它也由终结器在using块结束时调用,并在该变量范围结束时调用using var

来自Microsoft.Office.Interop.ExcelApplicationWorkbookWorksheet的顶级类获得自己的包装类,其中每个类都是DisposableComObject的子类

以下是代码:

/// <summary>/// References to COM objects must be explicitly released when done./// Failure to do so can result in odd behavior and processes remaining running after the application has stopped./// This class helps to automate the process of disposing the references to COM objects./// </summary>public abstract class DisposableComObject : IDisposable{public class ComObjectAccessedAfterDisposeException : Exception{public ComObjectAccessedAfterDisposeException() : base("COM object has been accessed after being disposed") { }}
/// <summary>The actual COM object</summary>private object ComObjectRef { get; set; }
/// <summary>The COM object to be used by subclasses</summary>/// <exception cref="ComObjectAccessedAfterDisposeException">When the COM object has been disposed</exception>protected object ComObject => ComObjectRef ?? throw new ComObjectAccessedAfterDisposeException();
public DisposableComObject(object comObject) => ComObjectRef = comObject;
/// <summary>/// True, if the COM object has been disposed./// </summary>protected bool IsDisposed() => ComObjectRef is null;
public void Dispose(){Dispose(true);GC.SuppressFinalize(this); // in case a subclass implements a finalizer}
/// <summary>/// This method releases the COM object and removes the reference./// This allows the garbage collector to clean up any remaining instance./// </summary>/// <param name="disposing">Set to true</param>protected virtual void Dispose(bool disposing){if (!disposing || IsDisposed()) return;Marshal.ReleaseComObject(ComObject);ComObjectRef = null;}
~DisposableComObject(){Dispose(true);}}

还有一个方便的泛型子类,使使用稍微容易一些。

public abstract class DisposableComObject<T> : DisposableComObject{protected new T ComObject => (T)base.ComObject;
public DisposableComObject(T comObject) : base(comObject) { }}

最后,我们可以使用DisposableComObject<T>为Excel互操作类创建包装类。

ExcelApplication子类具有对新Excel应用程序实例的引用,用于打开工作簿。

OpenWorkbook()返回一个ExcelWorkbook,它也是DisposableComObject的子类。

Dispose()已被重写以在调用baseDispose()方法之前退出Excel应用程序。Quit()Dispose()的别名。

public class ExcelApplication : DisposableComObject<Application>{public class OpenWorkbookActionCancelledException : Exception{public string Filename { get; }
public OpenWorkbookActionCancelledException(string filename, COMException ex) : base($"The workbook open action was cancelled. {ex.Message}", ex) => Filename = filename;}
/// <summary>The actual Application from Interop.Excel</summary>Application App => ComObject;
public ExcelApplication() : base(new Application()) { }
/// <summary>Open a workbook.</summary>public ExcelWorkbook OpenWorkbook(string filename, bool readOnly = false, string password = null, string writeResPassword = null){try{var workbook = App.Workbooks.Open(Filename: filename, UpdateLinks: (XlUpdateLinks)0, ReadOnly: readOnly, Password: password, WriteResPassword: writeResPassword, );
return new ExcelWorkbook(workbook);}catch (COMException ex){// If the workbook is already open and the request mode is not read-only, the user will be presented// with a prompt from the Excel application asking if the workbook should be opened in read-only mode.// This exception is raised when when the user clicks the Cancel button in that prompt.throw new OpenWorkbookActionCancelledException(filename, ex);}}
/// <summary>Quit the running application.</summary>public void Quit() => Dispose(true);
/// <inheritdoc/>protected override void Dispose(bool disposing){if (!disposing || IsDisposed()) return;App.Quit();base.Dispose(disposing);}}

ExcelWorkbook也是DisposableComObject<Workbook>的子类,用于打开工作表。

Worksheet()方法返回ExcelWorksheet,你猜对了,它也是DisposableComObject<Workbook>的子类。

Dispose()方法被覆盖,并在调用baseDispose()之前先关闭工作表。

注意:我添加了一些用于迭代Workbook.Worksheets的扩展方法。如果您遇到编译错误,这就是原因。我将在最后添加扩展方法。

public class ExcelWorkbook : DisposableComObject<Workbook>{public class WorksheetNotFoundException : Exception{public WorksheetNotFoundException(string message) : base(message) { }}
/// <summary>The actual Workbook from Interop.Excel</summary>Workbook Workbook => ComObject;
/// <summary>The worksheets within the workbook</summary>public IEnumerable<ExcelWorksheet> Worksheets => worksheets ?? (worksheets = Workbook.Worksheets.AsEnumerable<Worksheet>().Select(w => new ExcelWorksheet(w)).ToList());private IEnumerable<ExcelWorksheet> worksheets;
public ExcelWorkbook(Workbook workbook) : base(workbook) { }
/// <summary>/// Get the worksheet matching the <paramref name="sheetName"/>/// </summary>/// <param name="sheetName">The name of the Worksheet</param>public ExcelWorksheet Worksheet(string sheetName) => Worksheet(s => s.Name == sheetName, () => $"Worksheet not found: {sheetName}");
/// <summary>/// Get the worksheet matching the <paramref name="predicate"/>/// </summary>/// <param name="predicate">A function to test each Worksheet for a macth</param>public ExcelWorksheet Worksheet(Func<ExcelWorksheet, bool> predicate, Func<string> errorMessageAction) => Worksheets.FirstOrDefault(predicate) ??  throw new WorksheetNotFoundException(errorMessageAction.Invoke());
/// <summary>/// Returns true of the workbook is read-only/// </summary>public bool IsReadOnly() => Workbook.ReadOnly;
/// <summary>/// Save changes made to the workbook/// </summary>public void Save(){Workbook.Save();}
/// <summary>/// Close the workbook and optionally save changes/// </summary>/// <param name="saveChanges">True is save before close</param>public void Close(bool saveChanges){if (saveChanges) Save();Dispose(true);}
/// <inheritdoc/>protected override void Dispose(bool disposing){if (!disposing || IsDisposed()) return;Workbook.Close();base.Dispose(disposing);}}

最后是ExcelWorksheet

UsedRows()只是返回一个可枚举的未包装的Microsoft.Office.Interop.Excel.Range对象。我还没有遇到过从Microsoft.Office.Interop.Excel.Worksheet对象的属性访问的COM对象需要像ApplicationWorkbookWorksheet那样手动包装的情况。这些似乎都能自动清理它们。大多数情况下,我只是在迭代Ranges并获取或设置值,所以我的特定用例没有可用功能那么高级。

在这种情况下没有覆盖Dispose(),因为不需要对工作表执行特殊操作。

public class ExcelWorksheet : DisposableComObject<Worksheet>{/// <summary>The actual Worksheet from Interop.Excel</summary>Worksheet Worksheet => ComObject;
/// <summary>The worksheet name</summary>public string Name => Worksheet.Name;
// <summary>The worksheets cells (Unwrapped COM object)</summary>public Range Cells => Worksheet.Cells;
public ExcelWorksheet(Worksheet worksheet) : base(worksheet) { }
/// <inheritdoc cref="WorksheetExtensions.UsedRows(Worksheet)"/>public IEnumerable<Range> UsedRows() => Worksheet.UsedRows().ToList();}

可以添加更多的包装类。只需根据需要向ExcelWorksheet添加其他方法并在包装类中返回COM对象。只需复制我们通过ExcelApplication.OpenWorkbook()ExcelWorkbook.WorkSheets包装工作簿时所做的操作。

一些有用的扩展方法:

public static class EnumeratorExtensions{/// <summary>/// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>/// </summary>public static IEnumerable<T> AsEnumerable<T>(this IEnumerable enumerator){return enumerator.GetEnumerator().AsEnumerable<T>();}
/// <summary>/// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>/// </summary>public static IEnumerable<T> AsEnumerable<T>(this IEnumerator enumerator){while (enumerator.MoveNext()) yield return (T)enumerator.Current;}
/// <summary>/// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>/// </summary>public static IEnumerable<T> AsEnumerable<T>(this IEnumerator<T> enumerator){while (enumerator.MoveNext()) yield return enumerator.Current;}}
public static class WorksheetExtensions{/// <summary>/// Returns the rows within the used range of this <paramref name="worksheet"/>/// </summary>/// <param name="worksheet">The worksheet</param>public static IEnumerable<Range> UsedRows(this Worksheet worksheet) =>worksheet.UsedRange.Rows.AsEnumerable<Range>();}