如何正确清理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个回答

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
接受的答案对我没有用。析构函数中以下代码解决了问题。
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(); }
}

http://social.msdn.microsoft.com/Forums/en-US/3cd1ac57-37ab-45e9-918b-7b253052c836/problem-in-closing-the-excelexe-which-is-opened-by-c?forum=csharplanguage - Martin

1
我的回答有些晚,但仅旨在支持Govert提出的解决方案。
简短版本:
- 编写一个没有全局变量和参数的本地函数来执行COM操作。 - 在一个包装函数中调用COM函数并进行清理。
详细版:
您没有使用.NET来计算COM对象的引用并按正确顺序释放它们。即使是C++程序员也不再使用智能指针来做到这一点。因此,请忘记Marshal.ReleaseComObject和有趣的一个点好两个点坏的规则。如果您将不再需要的COM对象的所有引用设置为null,则GC会很高兴地执行释放COM对象的任务。最简单的方法是在本地函数中处理COM对象,其中所有用于COM对象的变量自然在末尾超出范围。由于调试器的某些奇怪特性在Hans Passant的杰出答案中指出,在接受的答案Post Mortem中,清理应委托给还调用执行函数的包装函数。因此,像Excel或Word这样的COM对象需要两个函数,一个执行实际工作的函数和一个包装器,该包装器调用此函数并在之后调用GC,就像Govert所做的那样,这是本主题中唯一正确的答案。为了展示原理,我使用了适用于所有执行COM操作的函数的包装器。除此扩展之外,我的代码只是Govert代码的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 seconds
      Thread.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);
    }
  }
}

0

到目前为止,似乎所有的答案都涉及以下一些内容:

  1. 终止进程
  2. 使用GC.Collect()
  3. 跟踪每个COM对象并正确释放它。

这让我很欣赏这个问题有多么困难 :)

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

我正在编写扩展方法来使生活更轻松,而不是直接在Interop提供的接口上编写。比如ApplicationHelpers.CreateExcel()或workbook.CreateWorksheet("mySheetNameThatWillBeValidated")。当然,任何创建的东西都可能导致以后清理时出现问题,所以我实际上更倾向于将终止进程作为最后的手段。然而,正确清理(第三个选项)可能是最不破坏性和最可控的。

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

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

我使用了“可释放的(Releasible)”来避免与“可处理的(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;
    }

我期望每个容器都会被垃圾回收器正确地销毁,从而自动调用QuitMarshal.FinalReleaseComObject

有意见吗?还是这是对第三种问题的回答?


0

我真的很喜欢事情自动清理...所以我写了一些包装类来为我完成所有的清理工作!这些类在下面有更详细的文档。

最终的代码非常易读和可访问。我还没有发现任何幽灵实例在我Close()工作簿和Quit()应用程序之后仍在运行(除了我在进程中调试和关闭应用程序的情况)。

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-up
  workbook1.Close(false);
  workbook2 .Close(true);
  excel.Quit();
}

注意:您可以跳过Close()Quit()调用,但如果您要写入Excel文档,则至少需要Save()。当对象超出范围(方法返回)时,类终结器将自动启动并进行任何清理。只要您在变量的作用域方面小心,例如仅在存储对COM对象的引用时将变量保持本地到当前作用域,那么来自Worksheet COM对象的任何对COM对象的引用都将自动管理和清除。如果需要,您可以轻松地将所需值复制到POCO中,或者根据下面讨论的内容创建其他包装类。
为了管理所有这些,我创建了一个名为DisposableComObject的类,它充当任何COM对象的包装器。它实现了IDisposable接口,并且还包含一个终结器,适用于那些不喜欢using的人。 Dispose()方法调用Marshal.ReleaseComObject(ComObject),然后将ComObjectRef属性设置为null。
当私有的ComObjectRef属性为null时,对象处于已处理状态。

如果在已释放后访问ComObject属性,则会抛出ComObjectAccessedAfterDisposeException异常。

Dispose()方法可以手动调用。它也会在using块结束时,以及在using var变量作用域结束时由终结器调用。

Microsoft.Office.Interop.Excel中的顶级类ApplicationWorkbookWorksheet都有自己的包装类,每个类都是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()已被重写以在调用基本Dispose()方法之前退出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() 方法被重写,并在调用基本的 Dispose() 之前首先关闭工作表。

注意:我添加了一些扩展方法,用于迭代 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时需要的那样。这些对象都似乎会自动清理自己。主要是我正在迭代区域并获取或设置值,所以我的特定用例不如可用功能那么高级。

在这种情况下,没有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>();
}

0

我在我的VSTO AddIn中新建Application对象后,也遇到了PowerPoint无法关闭的问题。我尝试了这里所有的答案,但效果有限。

这是我找到的解决方案 - 不要使用'new Application',ThisAddIn的AddInBase基类已经有一个对'Application'的句柄。如果您在需要它的地方使用该句柄(如果必须使其静态),那么您就不需要担心清理它,并且PowerPoint在关闭时不会挂起。


0

0

在这里再添加一个解决方案,使用C++/ATL自动化(我想你可以从VB/C#中使用类似的东西??)

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

这对我来说非常完美...


0

使用:

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

0

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

  1. 在打开Excel应用程序之前,获取所有名为oldProcessIds的进程ID。
  2. 打开Excel应用程序。
  3. 现在获取所有名为nowProcessIds的Excel应用程序进程ID。
  4. 需要退出时,杀死oldProcessIds和nowProcessIds之间的除外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;
            }
    
        }
    

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