Обработка iDisposable в сбойном инициализаторе или конструкторе - PullRequest
6 голосов
/ 29 сентября 2010

Есть ли в .Net хороший шаблон для гарантии того, что поля iDisposable, принадлежащие объекту, будут удалены, если в процессе создания возникнет исключение, возможно, во время инициализации поля? Единственный способ окружить инициализаторы полей в блоке Try / Catch - это если блок находится за пределами вызова конструктора, что затруднит правильное удаление кода очистки кодом очистки.

Единственный подход, который я могу представить, - это наследование объекта от базового класса, конструктор которого принимает нечто вроде массива iDisposable и устанавливает первый элемент в этом массиве, чтобы он указывал на себя. Все конструкторы классов-потомков должны быть Private или Orotected и включать этот параметр. Создание экземпляров должно осуществляться с помощью фабричных методов, которые объявят массив из одного iDisposable и передадут его соответствующему конструктору. Если конструктор терпит неудачу, метод фабрики будет иметь ссылку на частично сконструированный объект, который затем может быть удален (метод dispose должен, конечно, быть готовым принять возможность того, что объект может быть не полностью построен).

Подход может быть расширен, если объект хранит список объектов iDisposable, которые он создает, чтобы объекты могли быть очищены без необходимости явного удаления каждого из них; такой список был бы полезен в сочетании с подходом фабрики-метода-вызова-удаления, но в значительной степени ортогональн к нему.

Есть мысли?

Ответы [ 5 ]

12 голосов
/ 30 сентября 2010

Вы должны перехватить все исключения в конструкторе, затем утилизировать ваши дочерние объекты, а затем повторно выбросить исходное исключение (или новое исключение, предоставляющее дополнительную информацию).

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;
    }
  }
}
1 голос
/ 14 октября 2010

Я придумала шаблон, который кажется довольно хорошим. Это вдохновлено кем-то, размещенным на CodeProject.com - использующим список для отслеживания одноразовых товаров; raiiBase (из T) - это базовый класс, подходящий для любого класса, конструктор которого принимает один параметр. Конструктор класса должен быть защищен, а конструирование должно выполняться методом фабрики. Статический конструктор makeRaii () доставляет делегат к фабричной функции, которая должна принимать стек (из iDisposable) вместе с параметром ожидаемого типа класса. Пример использования:

Public Class RaiiTest
    Inherits 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)
    End Sub

    Private Shared Function _newRaiiTest(ByVal dispList As Stack(Of IDisposable), ByVal theName As String) As RaiiTest
        Return New RaiiTest(dispList, theName)
    End Function

    Public Shared Function newRaiiTest(ByVal theName As String) As RaiiTest
        Return makeRaii(Of RaiiTest)(AddressOf _newRaiiTest, theName)
    End Function

    Shared Sub test(ByVal st As String)
        Try
            Using it As RaiiTest = newRaiiTest(st)
                Debug.Print("Now using object")
            End Using
            Debug.Print("No exceptions thrown")
        Catch ex As raiiException
            Debug.Print("Output exception: " & ex.Message)
            If ex.InnerException IsNot Nothing Then Debug.Print("Inner exception: " & ex.InnerException.Message)
            For Each exx As Exception In ex.DisposalExceptions
                Debug.Print("Disposal exception: " & exx.Message)
            Next
        Catch ex As Exception
            Debug.Print("Misc. exception: " & ex.Message)
        End Try
    End Sub
End Class

Поскольку raiiTest наследует raiiBase (из String), для создания экземпляра класса вызовите newRaiiTest со строковым параметром. RAII () - это универсальная функция, которая регистрирует свой аргумент как iDisposable, который необходимо очистить, а затем вернуть его. Все зарегистрированные одноразовые материалы будут утилизироваться при вызове Dispose для основного объекта или при возникновении исключения в конструкции основного объекта.

Вот класс riaaBase:

Option Strict On
Class raiiException
    Inherits Exception
    ReadOnly _DisposalExceptions() As Exception
    Sub New(ByVal message As String, ByVal InnerException As Exception, ByVal allInnerExceptions As Exception())
        MyBase.New(message, InnerException)
        _DisposalExceptions = allInnerExceptions
    End Sub
    Public Overridable ReadOnly Property DisposalExceptions() As Exception()
        Get
            Return _DisposalExceptions
        End Get
    End Property
End Class

Public Class raiiBase(Of T)
    Implements IDisposable

    Protected raiiList As Stack(Of IDisposable)
    Protected creationParam As T
    Delegate Function raiiFactory(Of TT As raiiBase(Of T))(ByVal theList As Stack(Of IDisposable), ByVal theParam As T) As TT

    Shared Function CopyFirstParamToSecondAndReturnFalse(Of TT)(ByVal P1 As TT, ByRef P2 As TT) As Boolean
        P2 = P1
        Return False
    End Function

    Shared Function makeRaii(Of TT As raiiBase(Of T))(ByVal theFactory As raiiFactory(Of TT), ByVal theParam As T) As TT
        Dim dispList As New Stack(Of IDisposable)
        Dim constructionFailureException As Exception = Nothing
        Try
            Return theFactory(dispList, theParam)
        Catch ex As Exception When CopyFirstParamToSecondAndReturnFalse(ex, constructionFailureException)
            ' The above statement let us find out what exception occurred without having to catch and rethrow
            Throw ' Should never happen, since we should have returned false above
        Finally
            If constructionFailureException IsNot Nothing Then
                zapList(dispList, constructionFailureException)
            End If
        End Try
    End Function

    Protected Sub New(ByVal DispList As Stack(Of IDisposable), ByVal Params As T)
        Me.raiiList = DispList
        Me.creationParam = Params
    End Sub

    Public Shared Sub zapList(ByVal dispList As IEnumerable(Of IDisposable), ByVal triggerEx As Exception)
        Using theEnum As IEnumerator(Of IDisposable) = dispList.GetEnumerator
            Try
                While theEnum.MoveNext
                    theEnum.Current.Dispose()
                End While
            Catch ex As Exception
                Dim exList As New List(Of Exception)
                exList.Add(ex)
                While theEnum.MoveNext
                    Try
                        theEnum.Current.Dispose()
                    Catch ex2 As Exception
                        exList.Add(ex2)
                    End Try
                End While
                Throw New raiiException("RAII failure", triggerEx, exList.ToArray)
            End Try
        End Using
    End Sub

    Function RAII(Of U As IDisposable)(ByVal Thing As U) As U
        raiiList.Push(Thing)
        Return Thing
    End Function

    Shared Sub zap(ByVal Thing As IDisposable)
        If Thing IsNot Nothing Then Thing.Dispose()
    End Sub

    Private raiiBaseDisposeFlag As Integer = 0 ' To detect redundant calls

    ' IDisposable
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If disposing AndAlso Threading.Interlocked.Exchange(raiiBaseDisposeFlag, 1) = 0 Then
            zapList(raiiList, Nothing)
        End If
    End Sub

#Region " IDisposable Support "
    ' This code added by Visual Basic to correctly implement the disposable pattern.
    Public Sub Dispose() Implements IDisposable.Dispose
        ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub
#End Region

End Class

Обратите внимание, что пользовательский тип исключения будет выдан, если удаление не удастся для любого или всех зарегистрированных одноразовых объектов. InnerException будет указывать, не сработал ли конструктор; чтобы узнать, какие диспенсеры не удалось, проверьте DisposalExceptions.

1 голос
/ 29 сентября 2010

Держаться за частично построенный объект звучит для меня опасно, даже если это сработает. Я бы не использовал инициализаторы или 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??
}

0 голосов
/ 20 января 2011

Как ни странно, но похоже, что GC все еще вызывает деструктор для объектов IDisposable, даже если они выдают исключение в конструкторе! :)

using (crazy = new MyDisposable()) <-- constructor throws
{
} <-- dispose wont get called

... somewhen in far future
~MyDisposable() <-- GC kicks in.

Если вы были достаточно умны, чтобы использовать пример из msdn, где они вызвали Dispose (false) от деструктора - хорошо - вы просто потерпели неудачу! :)

0 голосов
/ 30 сентября 2010

В C # вы бы использовали 'using':

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

VB также имеет конструкцию Using ... End Using. При использовании этих методов метод Dispose гарантированно вызывается даже в случае исключения. Вы можете освободить любые ресурсы, созданные инициализаторами (или конструктором) в методе Dispose.

...