2010-09-29 5 views
6

Existe-t-il un motif sympa dans .Net pour s'assurer que les champs iDisposable appartenant à un objet seront éliminés si une exception est levée pendant la construction, éventuellement lors d'un initialiseur de champ? La seule façon d'entourer les initialiseurs de champ dans un bloc Try/Catch est si le bloc est en dehors de l'appel au constructeur, ce qui rendra le code de nettoyage plutôt difficile à éliminer correctement. La seule approche que je peux comprendre serait à l'objet hérité d'une classe de base dont le constructeur prend quelque chose comme un tableau de iDisposable, et définit le premier élément de ce tableau à pointer sur lui-même. Tous les constructeurs les classes descendantes doivent être Private ou Orotected et inclure ce paramètre. L'instanciation doit se faire via des méthodes d'usine, qui déclareront un tableau d'un iDisposable et le transmettront au constructeur approprié. Si le constructeur échoue, la méthode d'usine fera référence à l'objet partiellement construit, dont il peut alors disposer (la méthode d'élimination doit, bien sûr, être prête à accepter la possibilité que l'objet ne soit pas entièrement construit).Gestion de iDisposable dans un initialiseur ou un constructeur défaillant

L'approche pourrait être étendue en faisant en sorte que l'objet conserve une liste d'objets iDisposable qu'il crée, pour permettre aux objets d'être nettoyés sans avoir à les éliminer explicitement; une telle liste serait utile en conjonction avec l'approche usine-méthode-appels-élimination, mais elle est largement orthogonale à celle-ci.

Des pensées?

Répondre

1

J'ai trouvé un motif qui semble plutôt bien. Il est inspiré par quelqu'un posté sur CodeProject.com - en utilisant une liste pour garder une trace des consommables; raiiBase (de T) est une classe de base adaptée à toute classe dont le constructeur prend un seul paramètre. Le constructeur de la classe doit être protégé et la construction doit être effectuée par la méthode usine. Le constructeur statique makeRaii() prend un délégué à une fonction d'usine, qui doit accepter une pile (de iDisposable) avec un paramètre du type attendu de la classe.Une utilisation de l'échantillon:

 
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 

Depuis raiiTest raiiBase hérite (String), pour créer une instance de classe, appelez newRaiiTest avec un paramètre de chaîne. RAII() est une fonction générique qui va enregistrer son argument en tant que iDisposable qui aura besoin d'être nettoyé, puis le retourner. Tous les objets jetables enregistrés seront éliminés lorsque Dispose est appelé sur l'objet principal ou lorsqu'une exception est générée dans la construction de l'objet principal.

est ici la classe 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 

Notez qu'un type d'exception personnalisée sera levée si l'élimination échoue pour tout ou partie des objets à usage unique enregistrés. InnerException indiquera si le constructeur a échoué; pour voir quel (s) broyeur (s) a échoué, vérifiez DisposalExceptions.

1

Tenir un objet partiellement construit me semble dangereux, même si cela fonctionne. Je n'utiliserais pas d'initialiseurs ou de ctor pour gérer ça.

Que diriez-vous si à la place, vous utilisez une fabrique d'objets (pas tout à fait identique à une usine de classe) pour créer votre objet.

Le constructeur de votre objet ne serait pas responsable de la création des objets IDisposable qu'il possède. Au lieu de cela, l'usine crée chaque IDisposable et appelle le constructeur sur votre objet propriétaire. L'usine définirait ensuite les membres appropriés dans l'objet propriétaire sur les objets jetables créés.

pseudocode:

 

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# vous utiliseriez 'utilisant':

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

VB a également une aide ... Fin Utiliser la construction. Lors de leur utilisation, la méthode Dispose est garantie d'être appelée, même en cas d'exception. Vous pouvez libérer toutes les ressources créées par les initialiseurs (ou le constructeur) dans la méthode Dispose.

+3

-1 Cela ne fonctionne pas si le constructeur lève une exception. Le nouvel objet n'est jamais retourné, donc il n'y a rien à appeler Dispose. – chilltemp

+0

Hmmmm ... éclaté. –

12

Vous devez intercepter toutes les exceptions dans le constructeur, puis disposer de vos objets enfants, puis renvoyer l'exception d'origine (ou une nouvelle exception fournissant des informations supplémentaires).

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

Existe-t-il un moyen d'intercepter les exceptions dans les initialiseurs (par exemple "Font myFont = New Font (" Arial ", ... quelquechose ...);")? Dans de nombreux cas, il est beaucoup plus pratique de créer des objets lorsqu'ils sont définis que de les définir au même endroit, puis de les créer ailleurs. – supercat

+1

@supercact: Pas que je sache. Je suis d'accord avec la simplicité que vous recherchez, mais dans ce cas être robuste est plus important. – chilltemp

+0

+1 mais je préfère * not * appeler 'Dispose' dans le constructeur, car si vous implémentez le pattern canonique' IDisposable', vous pourriez vous retrouver avec un appel virtuel lors de l'appel du constructeur (via 'protected void Dispose (Bool Disposing) '). –

0

Aussi étrange que cela puisse paraître, mais il semble que GC appelle toujours le destructeur pour les objets IDisposable, même s'ils lancent une exception dans le constructeur! :)

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

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

Si vous étiez assez intelligent pour utiliser par exemple de msdn, où ils ont appelé Dispose (false) de destructor - bien - vous venez de perdre! :)

+2

Certains objets IDisposables se comporteront de manière acceptable même s'ils ne sont pas éliminés, car un finaliseur se déclenchera de manière suffisamment rapide pour les nettoyer. Il existe d'autres situations, cependant, si un objet a échoué, il peut provoquer une fuite de mémoire importante de durée indéterminée (par exemple, un abonné à des événements qui se réfère à de nombreux autres objets et s'abonne à un événement à long terme). Si cet objet ne se déclenche pas, ni cet objet, ni aucun objet auquel il renferme une référence, ne peut être collecté avant la mort de l'objet à longue durée de vie.) – supercat

+0

Dans vb.net, j'ai créé un motif pour les objets jetables qui permettent la déclaration, l'initialisation et le nettoyage des objets, à gérer sur une seule ligne (voir ci-dessous). Il nécessite que les initialiseurs de champs soient exécutés après le constructeur de base, et ne soit donc probablement pas adaptable à C#. – supercat

Questions connexes