在构造函数中调用Dispose方法,该方法抛出异常或意外行为

时间:2015-06-22 20:50:13

标签: c# idisposable

我有一个类消耗一些非托管资源,我想确定性地释放它们并请求不要为手头的对象调用终结器。我的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");
         }
     } 

     // ...
} 

2 个答案:

答案 0 :(得分:4)

我本身不会有对象调用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()和一个终结者进行清理,要么它有一个或多个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();
  }
}

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

答案 1 :(得分:2)

您所描述的是C ++ / CLI编译器实现的模式,它应该是所有.NET语言的标准模式,但不是。 .NET未能指定失败的构造函数应该导致对部分构造的对象的def helper(): bar = get_some_value() yield bar save(bar) def foo(): do_something() for x in helper(): yield x do_something_else() # more code in between for x in helper(): yield x # finishing up 调用(并且任何合法的Dispose实现必须准备好处理这个)意味着很多各种对象必须要么使用工厂方法而不是构造函数,需要一个笨拙的两步构造序列,其中对象处于一个奇怪的状态" limbo"直到第二步完成,或采用"希望没有任何错误的错误"错误处理和清理的理念。

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

Dispose

并非完全可怕,但令人厌烦。 .NET Framework可以从允许类指定一个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; } 方法中受益匪浅,该方法将在完成最派生的构造函数和返回客户端代码之间调用,但由于没有这样的特性,所以#34;正式和#34;存在最好的人可以做的就是手动对其进行处理[我认为有一些可用于COM互操作的kludges可能会有所帮助,但我不知道它有多好支持]。