【问题标题】:Handling IDisposable in failed initializer or constructor在失败的初始化程序或构造函数中处理 IDisposable
【发布时间】:2010-09-29 19:26:26
【问题描述】:

.Net 中是否有任何好的模式可以确保如果在构造过程中(可能在字段初始化程序期间)抛出异常,对象拥有的 IDisposable 字段将被释放?在 Try/Catch 块中包围字段初始值设定项的唯一方法是,如果该块位于对构造函数的调用之外,这将使清理代码很难正确处理任何内容。

我能想到的唯一方法是从基类继承对象,该基类的构造函数采用类似IDisposable 的数组,并将该数组中的第一项设置为指向自身。后代类的所有构造函数都应该是 Private 或 Protected,并包含该参数。实例化应该通过工厂方法,它将声明一个包含一个IDisposable 的数组并将其传递给适当的构造函数。如果构造函数失败,工厂方法将引用部分构造的对象,然后可以将其释放(当然,释放方法必须准备好接受对象可能未完全构造的可能性)。

可以通过让对象保留它创建的 iDisposable 对象列表来扩展该方法,以允许清理对象而不必显式处置每个对象;这样的列表与 factory-method-calls-dispose 方法结合使用会很有用,但在很大程度上与之正交。

有什么想法吗?

【问题讨论】:

    标签: .net constructor idisposable


    【解决方案1】:

    您应该在构造函数中捕获任何异常,然后处置您的子对象,然后重新抛出原始异常(或提供附加信息的新异常)。

    public class SomethingDisposable : IDisposable
    {
      System.Diagnostics.Process disposableProcess;
      public SomethingDisposable()
      {
        try
        {
          disposableProcess = new System.Diagnostics.Process();
          // Will throw an exception because I didn't tell it what to start
          disposableProcess.Start();
        }
        catch
        {
          this.Dispose();
          throw;
        }
      }
    
      public void Dispose()
      {
        if (disposableProcess != null)
        {
          disposableProcess.Dispose();
          disposableProcess = null;
        }
      }
    }
    

    【讨论】:

    • 有没有办法在初始化程序中捕获异常(例如“Font myFont = New Font("Arial", ...whatever...);")?在许多情况下,在定义对象时创建对象要比在一个地方定义它们然后在其他地方创建它们要方便得多。
    • @supercat:我不知道。我同意您寻求的简单性,但在这种情况下,稳健更为重要。
    • +1 但我更喜欢 not 在构造函数中调用 Dispose,因为如果您实现所谓的规范 IDisposable 模式,那么您最终可能会得到一个构造函数调用期间的虚拟调用(通过protected void Dispose(bool disposing))。
    【解决方案2】:

    抓住一个部分构造的对象对我来说听起来很危险,如果它甚至可以工作的话。我不会使用初始化程序或 ctor 来处理这个问题。

    如果您使用对象工厂(与类工厂不太一样)来创建您的对象。

    对象的构造函数不负责创建它拥有的 IDisposable 对象。相反,工厂将创建每个 IDisposable 并调用所有者对象的构造函数。然后,工厂会将所有者对象中的适当成员设置为已创建的一次性对象。

    伪代码:

    public superobject CreateSuperObject() { IDisposable[] members = new IDisposable[n] try SuperObject o = new SuperObject() // init the iDisposable members, add each to the array, (you will probably also nee o.DisposableMember1 = new somethingdisposeable(); members[0] = o.DisposeableMember1 return o; catch // loop through the members array, disposing where not null // throw a new exception?? }

    【讨论】:

      【解决方案3】:

      我想出了一个看起来不错的模式。它的灵感来自某个人在 CodeProject.com 上发布的内容——使用列表来跟踪一次性用品; raiiBase(of T) 是一个基类,适用于其构造函数采用单个参数的任何类。必须保护类构造函数,并且必须通过工厂方法进行构造。静态 makeRaii() 构造函数接受一个工厂函数的委托,该函数必须接受一个 Stack(of iDisposable) 以及该类预期类型的​​参数。示例用法:

      公开课 RaiiTest 继承 raiiBase(Of String) Dim thing1 As testDisposable = RAII(New testDisposable("Moe" & creationParam, "a")) Dim thing2 As testDisposable = RAII(New testDisposable("Larry " & creationParam, "b")) Dim thing3 As testDisposable = RAII(New testDisposable("Shemp" & creationParam, "c")) Dim thing4 As testDisposable = RAII(New testDisposable("Curly" & creationParam, "d")) Protected Sub New(ByVal dispList As Stack(Of IDisposable), ByVal newName As String) MyBase.New(dispList, newName) 结束子 私有共享函数 _newRaiiTest(ByVal dispList As Stack(Of IDisposable), ByVal theName As String) As RaiiTest 返回新的 RaiiTest(dispList, theName) 结束功能 公共共享函数 newRaiiTest(ByVal theName As String) As RaiiTest 返回 makeRaii(Of RaiiTest)(AddressOf _newRaiiTest, theName) 结束功能 共享子测试(ByVal st As String) 尝试 使用它作为 RaiiTest = newRaiiTest(st) Debug.Print("现在使用对象") 结束使用 Debug.Print("没有抛出异常") 捕获 ex As raiiException Debug.Print("输出异常:" & ex.Message) If ex.InnerException IsNot Nothing Then Debug.Print("Inner exception: " & ex.InnerException.Message) 对于每个 exx 作为 ex.DisposalExceptions 中的异常 Debug.Print("处理异常:" & exx.Message) 下一个 抓住前任作为例外 Debug.Print("杂项异常:" & ex.Message) 结束尝试 结束子 结束类

      由于 raiiTest 继承 raiiBase(of String),要创建类实例,请使用字符串参数调用 newRaiiTest。 RAII() 是一个通用函数,它将其参数注册为需要清理的 iDisposable,然后返回它。当在主对象上调用 Dispose 或在主对象的构造中引发异常时,所有已注册的可处置对象都将被 Disposed。

      这是 riaaBase 类:

      选项严格开启 类 raiiException 继承异常 只读 _DisposalExceptions() 作为异常 Sub New(ByVal message As String, ByVal InnerException As Exception, ByVal allInnerExceptions As Exception()) MyBase.New(消息,InnerException) _DisposalExceptions = allInnerExceptions 结束子 公共可重写只读属性 DisposalExceptions() As Exception() 得到 返回 _DisposalExceptions 结束获取 结束属性 结束类 公共类 raiiBase(Of T) 实现 IDisposable 受保护的 raiiList 作为堆栈(IDisposable 的) 受保护的创作Param As T 委托函数 raiiFactory(Of TT As raiiBase(Of T))(ByVal theList As Stack(Of IDisposable), ByVal theParam As T) As TT 共享函数 CopyFirstParamToSecondAndReturnFalse(Of TT)(ByVal P1 As TT, ByRef P2 As TT) As Boolean P2 = P1 返回假 结束功能 共享函数 makeRaii(Of TT As raiiBase(Of T))(ByVal theFactory As raiiFactory(Of TT), ByVal theParam As T) As TT 将 dispList 调暗为新堆栈(IDisposable 的) Dim constructionFailureException As Exception = Nothing 尝试 返回 theFactory(dispList, theParam) 当 CopyFirstParamToSecondAndReturnFalse 时将 ex 作为异常捕获(例如,constructionFailureException) ' 上面的语句让我们无需捕获并重新抛出即可找出发生了什么异常 Throw ' 不应该发生,因为我们应该在上面返回 false 最后 If constructionFailureException IsNot Nothing Then zapList(dispList, constructionFailureException) 万一 结束尝试 结束功能 Protected Sub New(ByVal DispList As Stack(Of IDisposable), ByVal Params As T) Me.raiiList = DispList Me.creationParam = 参数 结束子 公共共享子 zapList(ByVal dispList As IEnumerable(Of IDisposable),ByVal triggerEx As Exception) 使用枚举作为 IEnumerator(Of IDisposable) = dispList.GetEnumerator 尝试 而 Enum.MoveNext theEnum.Current.Dispose() 结束时 抓住前任作为例外 将 exList 调暗为新列表(例外) exList.Add(ex) 而 Enum.MoveNext 尝试 theEnum.Current.Dispose() 将 ex2 捕获为异常 exList.Add(ex2) 结束尝试 结束时 Throw New raiiException("RAII 失败", triggerEx, exList.ToArray) 结束尝试 结束使用 结束子 函数 RAII(Of U As IDisposable)(ByVal Thing As U) As U raiiList.Push(事物) 退货 结束功能 Shared Sub zap(ByVal Thing As IDisposable) If Thing IsNot Nothing Then Thing.Dispose() 结束子 Private raiiBaseDisposeFlag As Integer = 0 ' 检测冗余调用 ' 一次性 受保护的可覆盖子处置(ByVal 处置为布尔值) 如果处理 AndAlso Threading.Interlocked.Exchange(raiiBaseDisposeFlag, 1) = 0 那么 zapList(raiiList,无) 万一 结束子 #Region " IDisposable 支持 " ' 此代码由 Visual Basic 添加以正确实现一次性模式。 Public Sub Dispose() 实现 IDisposable.Dispose ' 不要更改此代码。将清理代码放在上面的 Dispose(ByVal disposing As Boolean) 中。 处置(真) GC.SuppressFinalize(Me) 结束子 #结束区域 结束类

      请注意,如果任何或所有已注册的一次性对象的处置失败,将引发自定义异常类型。 InnerException 将指示构造函数是否失败;要查看哪些处理程序失败,请检查 DisposalExceptions。

      【讨论】:

        【解决方案4】:

        虽然看起来很奇怪,但看起来 GC 仍在为 IDisposable 对象调用析构函数,即使它们在构造函数中抛出异常! :)

        using (crazy = new MyDisposable()) <-- constructor throws
        {
        } <-- dispose wont get called
        
        ... somewhen in far future
        ~MyDisposable() <-- GC kicks in.
        

        如果你足够聪明,可以使用 msdn 中的示例,他们从析构函数中调用 Dispose(false) - 那么 - 你只是失败了! :)

        【讨论】:

        • 一些 IDisposable 对象即使没有被释放也会表现得可以接受,因为终结器会以可接受的及时方式触发以清理它们。但是,在其他情况下,未能正确 Dispose 对象可能会导致无限期的重大内存泄漏(例如,具有对许多其他对象的引用并从长期存在的对象订阅事件的事件订阅者使用 Dispose 来取消订阅该事件。如果 dispose 没有触发,则该对象及其引用的任何对象都不能被收集,直到长期存在的对象死亡。)
        • 在 vb.net 中,我提出了一种一次性对象模式,它允许在一行中处理对象声明、初始化和清理(见下文)。但是,它要求字段初始化程序在基本构造函数之后运行,因此可能不适用于 C#。
        【解决方案5】:

        在 C# 中,您将使用“使用”:

                using(DisposableObject obj = new DisposableObject()) {
                }
        

        VB 也有一个 Using...End Using 结构。 使用这些时,即使发生异常,也保证调用 Dispose 方法。您可以在 Dispose 方法中释放由初始化程序(或构造函数)创建的任何资源。

        【讨论】:

        • -1 如果构造函数抛出异常,这将不起作用。新对象永远不会返回,所以没有什么可以调用 Dispose。
        猜你喜欢
        • 1970-01-01
        • 2011-07-11
        • 1970-01-01
        • 1970-01-01
        • 2010-10-16
        • 1970-01-01
        • 2018-01-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多