2010-09-29 16 views
6

¿Hay algún patrón agradable en .Net para asegurar que los campos iDisposable propiedad de un objeto serán eliminados si se lanza una excepción durante la construcción, posiblemente durante un inicializador de campo? La única forma de rodear los inicializadores de campo en un bloque Try/Catch es si el bloque está fuera de la llamada al constructor, lo que dificultará bastante que el código de limpieza elimine cualquier cosa.Manejo de iDisposable en el inicializador o constructor fallido

El único enfoque que puedo encontrar es que el objeto herede de una clase base cuyo constructor toma algo así como una matriz de iDisposable, y establece el primer elemento en esa matriz para que apunte a sí mismo. Todos los constructores de las clases descendientes deben ser Privados u Orotados e incluyen ese parámetro. La instanciación debe realizarse a través de métodos de fábrica, que declarará una matriz de una iDisposable y la pasará al constructor apropiado. Si el constructor falla, el método de fábrica tendrá una referencia al objeto parcialmente construido, que luego puede eliminar (el método de eliminación debe, por supuesto, estar preparado para aceptar la posibilidad de que el objeto no esté completamente construido).

El enfoque podría ampliarse haciendo que el objeto conserve una lista de los objetos iDisposable que crea, para permitir que los objetos se limpien sin tener que disponer explícitamente de cada uno; una lista de este tipo sería útil junto con el enfoque de fábrica-método-llamada-disposición, pero es en gran medida ortogonal a ella.

¿Alguna idea?

Respuesta

1

He encontrado un patrón que parece bastante bueno. Está inspirado en alguien publicado en CodeProject.com, utilizando una lista para realizar un seguimiento de los desechables; raiiBase (de T) es una clase base adecuada para cualquier clase cuyo constructor toma un solo parámetro. El constructor de la clase debe estar protegido, y la construcción debe realizarse mediante el método de fábrica. El constructor estático makeRaii() toma un delegado a una función de fábrica, que debe aceptar una pila (de iDisposable) junto con un parámetro del tipo esperado de la clase.Un ejemplos de uso:

 
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 

Desde raiiTest hereda raiiBase (de cadena), para crear una instancia de clase, llamada newRaiiTest con un parámetro de cadena. RAII() es una función genérica que registrará su argumento como un iDisposable que necesitará limpieza, y luego lo devolverá. Todos los desechables registrados se Disiparán cuando se invoque la opción Dispose en el objeto principal o cuando se genere una excepción en la construcción del objeto principal.

Aquí está la clase 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 

Tenga en cuenta que un tipo de excepción personalizada se produce si falla por cualquier disposición o la totalidad de los objetos desechables registrados. InnerException indicará si el constructor falló; para ver qué triturador (es) falló, ver DesecharExcepciones.

1

Aferrarme a un objeto parcialmente construido me parece peligroso, si es que funciona. No usaría inicializadores ni un ctor para manejar esto.

¿Qué tal si en su lugar, utiliza una fábrica de objetos (no exactamente lo mismo que una fábrica de clase) para crear su objeto.

El constructor de su objeto no sería responsable de crear los objetos IDisposable que posee. En cambio, la fábrica crearía cada IDisposable y llamaría al constructor en su objeto propietario. La fábrica establecería los miembros adecuados en el objeto propietario para los objetos desechables que se crearon.

pseudocódigo:

 

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?? 
} 
 
-2

En C# que usaría 'usando':

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

VB también tiene un final Usando ... El uso de la construcción. Al utilizarlos, se garantiza que se llamará al método Dispose, incluso en el caso de una excepción. Puede liberar los recursos creados por los inicializadores (o el constructor) en el método Dispose.

+3

-1 Esto no funciona si el constructor lanza una excepción. El nuevo objeto nunca se devuelve, por lo que no hay nada para llamar Dispose on. – chilltemp

+0

Hmmmm ... reventado. –

12

Debería capturar cualquier excepción en el constructor, luego deshacerse de sus objetos secundarios, luego volver a lanzar la excepción original (o una nueva excepción que proporcione información adicional).

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; 
    } 
    } 
} 
+0

¿Hay alguna forma de detectar excepciones en los inicializadores (por ejemplo, "Font myFont = New Font (" Arial ", ... whatever ...);")? En muchos casos, es mucho más conveniente crear objetos cuando están definidos que definirlos en un lugar y luego crearlos en otro lugar. – supercat

+1

@supercat: No es que yo sepa. Estoy de acuerdo con la simplicidad que busca, pero en este caso ser más robusto es más importante. – chilltemp

+0

+1 pero prefiero * no * llamar a 'Dispose' en el constructor, porque si implementa el llamado patrón' IDisposable' canónico, entonces puede terminar con una llamada virtual durante la llamada del constructor (a través de 'protected void Dispose (bool disponer) '). –

0

Por extraño que parezca, pero parece que GC todavía llama a destructor para objetos IDisposables, ¡incluso si lanzan una excepción en el constructor! :)

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

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

Si usted fuera lo suficientemente inteligente como para usar el ejemplo de MSDN, donde llamaron a botar (falso) del destructor - bien - que acaba de fallar! :)

+2

Algunos objetos IDisposable se comportarán aceptablemente aunque no se eliminen, porque un finalizador se activará de forma aceptablemente oportuna para limpiarlos. Hay otras situaciones, sin embargo, si no se desecha correctamente, un objeto puede causar una fuga de memoria mayor de duración indefinida (por ejemplo, un suscriptor de eventos que tiene referencias a muchos otros objetos y se suscribe a un evento de un objeto de larga vida usa Dispose to anular la suscripción de ese evento. Si el desecho no se activa, ni ese objeto, ni ninguno del que tenga una referencia, pueden recolectarse hasta que muera el objeto de larga vida). – supercat

+0

En vb.net, he encontrado un patrón para objetos desechables que permite la declaración de objetos, la inicialización y la limpieza, para ser manejados en una sola línea (ver a continuación). Sin embargo, requiere que los inicializadores de campo se ejecuten después del constructor base y, por lo tanto, probablemente no se pueda adaptar a C#. – supercat

Cuestiones relacionadas