使用包装对象来正确清理Excel交互对象

11

所有这些问题:

在使用完Excel COM对象后,C#无法正确释放它们,这是一个常见的问题。解决此问题主要有两个方向:

  1. 在不再使用Excel时杀死Excel进程。
  2. 确保显式地将每个使用的COM对象分配给一个变量,并保证最终在每个对象上执行Marshal.ReleaseComObject。

有人表示,方法2太繁琐了,在代码中可能会忘记遵循此规则,存在一定的不确定性。而方法1似乎又很不可靠,而且容易出错,特别是在受限制的环境中试图杀死进程可能会引发安全错误。

所以我正在思考通过创建另一个代理对象模型来解决方法2,该模型模拟Excel对象模型(对我来说,只实现我实际需要的对象即可)。其原理如下:

  • 每个Excel互操作类都有其代理,它包装了该类的一个对象。
  • 代理在其终结器中释放COM对象。
  • 代理模拟互操作类的接口。
  • 任何最初返回COM对象的方法都更改为返回代理。其他方法只是将实现委托给内部COM对象。

示例:

public class Application
{
    private Microsoft.Office.Interop.Excel.Application innerApplication
        = new Microsoft.Office.Interop.Excel.Application innerApplication();

    ~Application()
    {
        Marshal.ReleaseCOMObject(innerApplication);
        innerApplication = null;
    }

    public Workbooks Workbooks
    {
        get { return new Workbooks(innerApplication.Workbooks); }
    }
}

public class Workbooks
{
    private Microsoft.Office.Interop.Excel.Workbooks innerWorkbooks;

    Workbooks(Microsoft.Office.Interop.Excel.Workbooks innerWorkbooks)
    {
        this.innerWorkbooks = innerWorkbooks;
    }

    ~Workbooks()
    {
        Marshal.ReleaseCOMObject(innerWorkbooks);
        innerWorkbooks = null;
    }
}

我的问题具体是:

  • 谁认为这是一个不好的想法,为什么?
  • 谁认为这是一个伟大的想法?如果是这样,为什么还没有人实现/发布这样的模型?这只是由于工作量,还是我忽略了这个想法的致命问题?
  • 在finalizer中使用ReleaseCOMObject是否不可能/错误/容易出错?(我只见过将其放在Dispose()而不是finalizer中的提案 - 为什么?)
  • 如果这种方法是有意义的,有什么建议可以改进它吗?
5个回答

5
“在析构函数中使用ReleaseCOMObject是否不可能/不好/危险?(我只看到建议将其放在Dispose()而不是析构函数中,为什么?)”
建议不要将清理代码放在终结器中,因为与C++中的析构函数不同,它不是确定性地调用。它可能在对象超出范围后不久被调用。也可能需要一个小时。它可能永远不会被调用。一般来说,如果想处理非托管对象,应该使用IDisposable模式,而不是终结器。
您提供的solution试图通过显式调用垃圾回收器并等待终结器完成来解决这个问题。一般情况下,这真的不推荐,但对于这种特殊情况,由于很难跟踪所有临时创建的非托管对象,有些人认为这是一种可接受的解决方案。但显式清理是正确的方法。然而,考虑到这样做的难度,这个“hack”可能是可以接受的。请注意,这个解决方案可能比您提出的想法更好。
如果您想尝试显式清理,"不要在COM对象中使用两个点"的指导方针将帮助您记住保留对每个创建的对象的引用,以便在完成后清除它们。

你对finalizer的原则是正确的,但在这种特定情况下,我可能会说:让我们将责任委托给垃圾回收器,因为由于finalizer我可以依赖GC也会释放COM对象,就在内部对象被销毁时。如果我想在代码的某个地方显式释放所有资源,我可以使用GC.Collect()。 - chiccodoro
@chiccodoro: 这种方法的问题在于它依赖于垃圾回收的具体实现细节。如果垃圾回收稍微改变,它可能就不再起作用了。但我理解在这种情况下做得恰当的困难,并且在某些情况下,这种黑客行为可能是最具成本效益的解决方案。 - Mark Byers
嗯,我明白你的意思。但是为每个单点使用using()太繁琐了。也许只需在Excel.Application上定义一个Dispose()方法,该方法运行GC.Collect(),以便在此时释放所有其他Excel对象?因为在Excel.Application离开“using”范围之前,我不需要程序释放来自应用程序的COM资源。 - chiccodoro
@chiccodoro:你可以创建一个包装类,让你可以访问Excel.Application对象,并在该类的Dispose中使用GC.Collect技巧。 - Mark Byers

2

1
看看我的项目MS Office for .NET。通过VB.NET的本地后期绑定能力,已经解决了关于引用包装对象和本地对象的问题。

嗨TcKs,你能详细说明一下吗?在项目页面中,你写道:“不需要MS Office Primary Interop Assemblies。”这是否意味着我们不需要直接通信其对象模型,因为包装器对象会处理它,还是说你进行了相当低级别的实现,完全替换了Interop程序集? - chiccodoro
是的,它绕过了InteropAssembly的标准方式。它在运行时动态检查COM对象模型。它比PIA慢一点,但不受版本影响(包括未来版本)。 - TcKs
这听起来非常有趣。--你最后一次更改代码和网页是在2008年? - chiccodoro
常见的目标可以通过当前或旧版本完成。我计划在Office 2010发布时重振项目。 - TcKs

0

就算是值得一提的是,codeplex上的Excel刷新服务使用了这个逻辑:

    public static void UsingCOM<T>(T reference, Action<T> doThis) where T : class
    {
        if (reference == null) return;
        try
        {
            doThis(reference);
        }
        finally
        {
            Marshal.ReleaseComObject(reference);
        }
    }

谢谢通知!但是这将会生成很多嵌套的回调函数,因此我认为与使用()方法相比并没有优势。 - chiccodoro

0

我会做什么:

class ScopedCleanup<T> : IDisposable where T : class
{
    readonly Action<T> cleanup;

    public ScopedCleanup(T o, Action<T> cleanup)
    {
        this.Object = o;
        this.cleanup = cleanup;
    }

    public T Object { get; private set; }

    #region IDisposable Members

    public void Dispose()
    {
        if (Object != null)
        {
            if(cleanup != null)
                cleanup(Object);
            Object = null;
            GC.SuppressFinalize(this);
        }
    }

    #endregion

    ~ScopedCleanup() { Dispose(); }
}

static ScopedCleanup<T> CleanupObject<T>(T o, Action<T> cleanup) where T : class
{
    return new ScopedCleanup<T>(o, cleanup);
}

static ScopedCleanup<ComType> CleanupComObject<ComType>(ComType comObject, Action<ComType> actionBeforeRelease) where ComType : class
{
    return
        CleanupObject(
            comObject,
            o =>
            {
                if(actionBeforeRelease != null)
                    actionBeforeRelease(o);
                Marshal.ReleaseComObject(o);
            }
        );
}

static ScopedCleanup<ComType> CleanupComObject<ComType>(ComType comObject) where ComType : class
{
    return CleanupComObject(comObject, null);
}

用例。请注意调用Quit,这似乎是必要的以结束进程:

using (var excel = CleanupComObject(new Excel.Application(), o => o.Quit()))
using (var workbooks = CleanupComObject(excel.Object.Workbooks))
    {
        ...
    }

嗨Rotaerk。这看起来像是IDisposable方法的一个有趣的增强,但它并没有回答我的问题。(或者我可能没理解到重点。) - chiccodoro
1
正如先前所提到的,使用IDisposable是正确的方法,而不是使用终结器(Finalizer)。终结器可以确保资源得到清理,但不能控制清理发生的时间。GC.Collect不能保证立即回收所有内容,因此这也不是解决方案。一个可释放对象会严格控制释放发生的时间。此外,using语句让您以异常安全的方式这样做。(只需记住在调试时不要过早停止应用程序的清理,否则它将无法完成,您最终会有一堆Excel进程在运行。) - Rotaerk
你看到剩余的对象是因为在调用Collect()之间你没有调用.WaitForPendingFinalizers(),就这么简单。 - Anonymous Type

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