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

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

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

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


当前回答

我的回答很晚,其唯一目的是支持戈弗特提出的解决方案。

简短版本:

编写一个没有全局变量和参数的局部函数执行COM内容。在调用COM的包装函数中调用COM函数功能,然后进行清洁。

长版本:

您没有使用.Net来计算COM对象的引用数,并以正确的顺序自行释放它们。即使C++程序员也不再使用智能指针来实现这一点。所以,忘掉Marshal.ReleaseComObject和有趣的一点好两点坏规则吧。如果您对不再需要的COM对象的所有引用都为空,GC很乐意做释放COM对象的工作。最简单的方法是在一个局部函数中处理COM对象,COM对象的所有变量在最后自然地超出了范围。由于Hans Passant的精彩回答中指出了调试器的一些奇怪特性,在Post-Mortem的公认答案中提到,清理应该委托给一个包装函数,该包装函数也调用执行函数。因此,像Excel或Word这样的COM对象需要两个函数,一个执行实际任务,一个包装器调用此函数,然后像Govert那样调用GC,这是本线程中唯一正确的答案。为了说明这个原理,我使用了一个适合所有做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);
    }
  }
}

其他回答

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

终止进程使用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的虚拟实例在运行(除了在调试和关闭应用程序的过程中)。

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对象中对COM对象的任何引用都将被自动管理和清理,例如,只有在存储对COM对象引用时,才将变量保持在当前作用域的本地。如果需要,您可以轻松地将所需的值复制到POCO,或者创建其他包装类,如下所述。

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

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

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

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

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

Microsoft.Office.Interop.Excel、Application、Workbook和Worksheet中的顶级类都有自己的包装类,每个包装类都是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对象需要手动包装,就像应用程序、工作簿和工作表需要那样。这些似乎都能自动清理它们。大多数情况下,我只是在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>();
}

前言:我的答案包含两个解决方案,所以阅读时要小心,不要错过任何东西。

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

显式释放EVERY 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次,并为所有内容添加了Marshal.FinalReleaseComObject()!我还有GC.Collect()和GC.WaitForPendingFinalizers()。我检查了隐藏的消息框。我试图将WM_CLOSE消息发送到Excel主窗口。我在一个单独的AppDomain中执行了我的函数,并卸载了该域。没什么帮助!关闭所有excel实例的选项是不合适的,因为如果用户在执行我的函数(该函数也适用于excel)期间手动启动另一个excel实例,则该实例也将被我的函数关闭。我打赌用户不会高兴的!所以,老实说,这是一个蹩脚的选择(没有冒犯的家伙)。所以我花了几个小时才找到了一个好的(我个人认为)解决方案:通过关闭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模式,我提供了两种方法(我认为这在这里是合适的):一种方法在无法终止进程时不会抛出异常(例如,进程不再存在),另一种方法则在进程未终止时抛出异常。此代码中唯一的弱点是安全权限。理论上,用户可能没有权限终止进程,但在所有情况下,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将关闭=)

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

我花了一些时间试图解决这个问题,当时XtremeVBTalk是最活跃和响应最快的。这里有一个链接到我的原始文章,“即使应用程序崩溃,也要干净地关闭Excel互操作过程”。下面是文章的摘要,以及复制到本文的代码。

使用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;被调用。0x2000是JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE枚举值,MSDN将此值定义为:

当作业的最后一个句柄关闭。

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

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

使用Microsoft Excel 2016测试

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

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

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

1包括课堂作业

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