【问题标题】:Calling Dispose method inside constructor which throws an exception or behaves unexpectedly在引发异常或行为异常的构造函数中调用 Dispose 方法
【发布时间】:2015-06-23 08:58:58
【问题描述】:

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

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

我可以在“失败”的构造函数中调用 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");
         }
     } 

     // ...
} 

【问题讨论】:

  • 注意,如果在构造函数中抛出异常,构造函数不会正常返回,所以构造的对象永远不会被传回,所以客户端代码不可能对其调用Dispose。跨度>
  • 是的,但我说的是构造函数本身。
  • @closers - 这基于意见。别傻了。
  • 推理一下,如果构造函数没有抛出会发生什么?您将保持该库加载一段时间。那是泄漏吗?制定精益崩溃计划有什么意义?听起来够疯狂吗?
  • 使用 SafeHandle,您可以忘记编写自己的 ~Destructor()。并保持您的构造函数简洁明了。更容易和更可靠。

标签: c# idisposable


【解决方案1】:

我不会对自己进行对象调用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() 可以做同样的伎俩,我主要更喜欢在相同的方法中显式地清理我正在清理的字段(构造函数这里)设置它但未能完成所有工作。一方面,可以有部分构造对象的唯一地方是在构造函数中,所以我需要考虑部分构造对象的唯一地方是在构造函数中;我已经使它成为_lib 不为空的类的其余部分的不变量。

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

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

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

【讨论】:

  • 在失败的情况下让构造函数负责自己的清理的方法可能比使用Dispose 用于非继承的非继承类“更干净”,但我不确定它是如何实现的除非基类定义了一个虚拟的“清理失败的构造函数”方法,否则可以扩展到涉及继承的场景。如果不是通过Dispose,你会如何建议派生类清理部分构造的基础?
  • @supercat 在这种情况下,我通常会把它留给终结者,我希望这种情况很少见。我认为base.Dispose() 是合理的(此时不应部分构建基础),除非它采用流行的虚拟Dispose(bool) 方法,在这种情况下它将回调派生类,那就是太痛苦了。
  • 如果继承合同指定可以在部分构造的对象上调用派生的Dispose(bool),这会带来什么痛苦?如果派生构造函数将至少一个字段设置为非默认值(通常应该是这种情况),我认为如果在没有派生构造函数的情况下调用Dispose(bool) 方法,它应该很容易简单地链接到父方法跑了。我认为在给定情况下使用终结器的必要性通常是相关设计缺陷的证据(有时没有其他修复方法......
  • ... 会很实用),并且建议即使适当的清理很尴尬,它几乎总是比“希望终结器可以处理它”更好,除非在处理可替代且丰富的资源时。
【解决方案2】:

您描述的是 C++/CLI 编译器实现的模式,它应该是所有 .NET 语言的标准,但不是。 .NET 未能指定失败的构造函数应导致在部分构造的对象上调用Dispose(并且任何合法的Dispose 实现必须准备好处理此问题)意味着许多类型的对象必须要么需要使用工厂方法而不是构造函数,要么需要一个尴尬的两步构造序列,其中对象处于奇怪的“边缘”直到第二步完成,或者采用“希望没有什么太严重的错误”的错误哲学处理和清理。

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

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 方法中受益匪浅可以做的可能是手动组合它[我认为有一些为 COM 互操作设计的组合可能会有所帮助,但我不知道有多少支持]。

【讨论】:

    猜你喜欢
    • 2016-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-30
    相关资源
    最近更新 更多