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

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

尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。

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


当前回答

尝试之后

按相反顺序释放COM对象在末尾添加两次GC.Collect()和GC.WaitForPendingFinalizers()不超过两个点关闭工作簿并退出应用程序以释放模式运行

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

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

我们将其添加到包装器的函数末尾,如下所示:

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

其他回答

到目前为止,似乎所有的答案都涉及其中一些:

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

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

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

我没有直接在Interop提供的接口上进行编写,而是使用扩展方法来简化工作。类似于ApplicationHelpers.CreateExcel()或工作簿.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);
    }
}

我用“不可行”来避免与一次性使用混淆。但将其扩展到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正确地销毁,因此会自动调用Quit和Marshal.FinalReleaseComObject。

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

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

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

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

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

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

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

在大多数从.NET清理COM对象的代码示例中,GC.Collect()和GC.WaitForPendingFinalizers()调用两次,如下所示:

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

但是,除非您使用的是Visual Studio Tools for Office(VSTO),该工具使用的终结器会导致整个对象图在终结队列中升级,否则这不应该是必需的。在下一次垃圾收集之前,不会释放此类对象。但是,如果您不使用VSTO,则应该能够调用GC.Collect()和GC.WaitForPendingFinalizers()一次。

我知道显式调用GC.Collect()是一个不允许的做法(当然,重复两次听起来很痛苦),但老实说,没有办法解决这个问题。通过正常操作,您将生成隐藏对象,这些对象没有引用,因此,除了调用GC.Collect()之外,您无法通过任何其他方式释放这些对象。

这是一个复杂的主题,但这确实是它的全部内容。一旦为清理过程建立了这个模板,您就可以正常编码,而不需要包装器等:-)

我在这里有一个教程:

用VB.Net/COM Interop实现Office程序的自动化

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

我认为其中一些只是框架处理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相关的对象!

我花了几个小时尝试了几种方法。所有这些都是好主意,但我终于发现了我的错误:如果你不释放所有对象,上面的任何方法都不能像我这样帮助你。确保释放所有对象,包括范围1!

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

这里有各种选项。

使用Microsoft Excel 2016测试

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

参考C#,请参见:https://stackoverflow.com/a/1307180/10442623

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

1包括课堂作业

2实现该类以处理excel进程的适当处理