在引发异常或行为异常的构造函数中调用 Dispose 方法

Calling Dispose method inside constructor which throws an exception or behaves unexpectedly

我有一个 class 消耗了一些非托管资源,我想确定性地释放它们并请求不要为手头的对象调用终结器。 class 的 Dispose() 方法实现了这一点。

如果抛出异常或出现其他错误或在构造函数中出现意外行为,我想在抛出之前调用 Dispose()。但是,我很少遇到捕获抛出的异常或处理一次性对象构造函数中的错误然后在对象上调用 Dispose() 的实现 - 在很多情况下,作者将清理留给终结器。我没有读到任何说明在失败的构造函数中调用 Dispose() 是不好的做法的内容,但是在查看 .NET 源代码时,我还没有在一次性对象构造函数中遇到过此类异常或错误处理。

我可以在 "failed" 构造函数中调用 Dispose() 并且仍然被认为是一个好的编码公民吗?

编辑澄清 - 我说的是构造函数内部:

public class MyClass : IDisposable
{
     private IntPtr _libPtr = IntPtr.Zero;

     public MyClass(string dllPath)
     {
         _libPtr = NativeMethods.LoadLibrary(dllPath);

         if (_libPtr != IntPtr.Zero)
         { 
             IntPtr fxnPtr = NativeMethods.GetProcAddress(_libPtr, "MyFunction");
             if (fxnPtr == IntPtr.Zero)
             {
                 Dispose(); // Cleanup resources - NativeMethods.FreeLibrary(_libPtr);
                 throw new NullReferenceException("Error linking library."); 
             }
         }
         else
         {
             throw new DllNotFoundException("Something helpful");
         }
     } 

     // ...
} 

您描述的是C++/CLI 编译器实现的模式,这应该是所有.NET 语言的标准,但不是。 .NET 未能指定失败的构造函数应导致对部分构造的对象调用 Dispose(并且任何合法的 Dispose 实现都必须准备好处理此问题)意味着许多种类的对象要么需要使用工厂方法而不是构造函数,要么需要笨拙的两步构造顺序,其中对象处于奇怪的 "limbo" 直到第二步完成,或者采用 "hope nothing goes too badly wrong wrong" 哲学用于错误处理和清理。

在这些方法中,最好的方法可能是要求使用工厂方法进行构造。因为工厂方法特定于正在创建的对象的类型,所以这种方法需要派生 类 来包含一些烦人的样板文件,效果如下:

DerivedFoo Create(params)
{
  // Phase 1 shouldn't allocate resources yet
  Derived foo result = new DerivedFoo(params);
  // NON-VIRTUAL Phase 2 method which chains to a virtual one within try/finally
  result.Initialize();
  return result;
}

不完全可怕,但令人厌烦。 .NET Framework 可以从允许 类 指定一个 Initialize 方法中受益匪浅,该方法将在最派生的构造函数完成和 return 客户端代码之间调用,但是由于不存在这样的功能 "officially" 最好的办法可能是手动拼凑它 [我认为有一些为 COM 互操作设计的拼凑可能会有所帮助,但我不知道它们的支持程度如何]。

我不会对自身进行对象调用 Dispose,但如果需要,我当然会让构造函数自行清理。我还想让清理工作尽可能简单考虑到您的示例,我更愿意将其编写为:

internal sealed class Library : IDisposable
{
  IntPtr _libPtr; // Or better yet, can we use or derive from SafeHandle?
  public Library(string dllPath)
  {
     _libPtr = NativeMethods.LoadLibrary(dllPath);
     if(_libPtr == IntPtr.Zero)
     {
       GC.SuppressFinalize(this);
       throw new DllNotFoundException("Library Load Failed");
     }
  }
  private void Release()
  {
    if(_libPtr != IntPtr.Zero)
      NativeMethods.FreeLibrary(_libPtr);
    _libPtr = IntPtr.Zero; // avoid double free even if a caller double-disposes.
  }
  public void Dispose()
  {
    Release();
    GC.SuppressFinalize(this);
  }
  ~Library()
  {
    Release();
  }
  public IntPtr GetProcAddress(string functionName)
  {
    if(_libPtr == IntPtr.Zero)
      throw new ObjectDisposedException();
    IntPtr funcPtr = NativeMethods.GetProcAddress(_libPtr, functionName);
    if(_funcPtr == IntPtr.Zero)
      throw new Exception("Error binding function.");
    return _funcPtr;
  }
}

到目前为止很简单。要么这个对象被成功构造并且可以被调用它的代码释放,要么它不需要清理。我们甚至可以防止无操作终结,只是为了友好。最主要的是,在最后一件可能会出错的事情之后,没有任何需要清理的东西。

然后:

public sealed class MyClass : IDisposable
{
  private readonly Library _lib;
  private readonly IntPtr _funcPtr;

  public MyClass(string dllPath)
  {
    _lib = new Library(dllPath); // If this fails, we throw here, and we don't need clean-up.

    try
    { 
      _funcPtr = _libPtr.GetProcAddress("MyFunction");
    }
    catch
    {
      // To be here, _lib must be valid, but we've failed over-all.
      _lib.Dispose();
      throw;
    }
  }
  public void Dispose()
  {
    _lib.Dispose();
  }
  // No finaliser needed, because no unmanaged resources needing finalisation are directly held.
}

同样,我可以确保清理,但我不调用 this.Dispose(); 虽然 this.Dispose() 可以做同样的事情,但我主要更喜欢让我正在清理的字段显式在设置它但未能完成其所有工作的相同方法(此处为构造函数)。一方面,唯一可以存在部分构造对象的地方是在构造函数中,因此我唯一需要考虑部分构造对象的地方是在构造函数中;我已将 class 的其余部分设为不变量,即 _lib 不为空。

让我们假设函数必须与库分开发布,只是为了有一个更复杂的例子。然后我也会包装 _funcPtr 以保持简化规则; class 有一个通过 Dispose() 清理的非托管资源和一个终结器,或者它有一个或多个 IDisposable 字段,它通过 Dispose 清理,或者它不需要处置,但绝不是以上的组合。

internal sealed class Function : IDisposable
{
  IntPtr _funcPtr; // Again better yet, can we use or derive from SafeHandle?
  public Function(Lib library, string functionName)
  {
    _funcPtr = library.GetProcAddress(functionName);
    if(_funcPtr == IntPtr.Zero)
    {
      GC.SuppressFinalize(this);
      throw new Exception("Error binding function."); 
    }
  }
  private void Release()
  {
    if(_funcPtr != IntPtr.Zero)
      NativeMethods.HypotheticalForgetProcAddressMethod(_funcPtr);
    _funcPtr = IntPtr.Zero; // avoid double free.
  }
  public void Dispose()
  {
    Release();
    GC.SuppressFinalize(this);
  }
  ~Function()
  {
    Release();
  }
}

然后 MyClass 将是:

public sealed class MyClass : IDisposable
{
  private Library _lib;
  private Function _func;

  public MyClass(string dllPath)
  {
    _lib = new Library(dllPath); // If this fails, we throw here, and we don't need clean-up.
    try
    { 
      _func = new Function(_lib, "MyFunction");
      try
      {
        SomeMethodThatCanThrowJustToComplicateThings();
      }
      catch
      {
        _func.Dispose();
        throw;
      }
    }
    catch
    {
      _lib.Dispose();
      throw;
    }
  }
  public void Dispose()
  {
    _func.Dispose();
    _lib.Dispose();
  }
}

这使得构造函数更加冗长,我宁愿避免两件可能出错的事情首先影响两件需要清理的事情。它确实反映了为什么我喜欢对不同领域进行明确的清理;我可能想清理两个字段,或只清理一个字段,具体取决于发生异常的位置。