2010-09-29 9 views
6

C'è qualche schema piacevole in .Net per garantire che i campi iDisposable di proprietà di un oggetto vengano eliminati se viene generata un'eccezione durante la costruzione, possibilmente durante un inizializzatore di campo? L'unico modo per circondare gli inizializzatori di campo in un blocco Try/Catch è se il blocco si trova all'esterno della chiamata al costruttore, il che renderà piuttosto difficile per il codice di pulizia smaltire correttamente qualsiasi cosa.Gestione di iDisposable nell'inizializzatore o nel costruttore non riuscito

L'unico approccio che posso capire sarebbe l'oggetto ereditato da una classe base il cui costruttore prende qualcosa come un array di iDisposable e imposta il primo elemento in quell'array in modo che punti a se stesso. Tutti i costruttori le classi discendenti dovrebbero essere Private o Orotected e includere tale parametro. L'istanziazione dovrebbe avvenire tramite metodi factory, che dichiareranno un array di un iDisposable e lo passeranno al costruttore appropriato. Se il costruttore fallisce, il metodo factory avrà un riferimento all'oggetto parzialmente costruito, che può quindi disporre (il metodo di smaltimento deve, ovviamente, essere pronto ad accettare la possibilità che l'oggetto non possa essere completamente costruito).

L'approccio può essere esteso avendo l'oggetto mantenere un elenco di oggetti iDisposable che crea, per consentire la pulizia degli oggetti senza doverli esplicitamente disporne; una tale lista sarebbe utile in congiunzione con l'approccio metodo-chiamata-disputa di fabbrica, ma è in gran parte ortogonale ad esso.

Qualche idea?

risposta

1

Ho ideato un modello che sembra abbastanza buono. È ispirato da qualcuno pubblicato su CodeProject.com - utilizzando un elenco per tenere traccia degli articoli usa e getta; raiiBase (of T) è una classe base adatta per qualsiasi classe il cui costruttore prende un singolo parametro. Il costruttore della classe deve essere protetto e la costruzione deve essere eseguita tramite il metodo factory. Il costruttore statico makeRaii() accetta un delegato in una funzione factory, che deve accettare uno Stack (di iDisposable) insieme a un parametro del tipo previsto della classe.Un esempio dell'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 

Dal raiiTest eredita raiiBase (stringa), per creare un'istanza di classe, chiamata newRaiiTest con un parametro di stringa. RAII() è una funzione generica che registrerà il suo argomento come iDisposable che dovrà essere ripulito e quindi restituirlo. Tutti i dispositivi usa e getta registrati verranno eliminati quando Dispose viene chiamato sull'oggetto principale o quando viene generata un'eccezione nella costruzione dell'oggetto principale.

Ecco 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 

Nota che un tipo di eccezione personalizzato verrà generata se lo smaltimento non riesce per uno o tutti gli oggetti usa e getta registrati. InnerException indicherà se il costruttore non è riuscito; per vedere quale/i dissipatore/i ha avuto esito negativo, selezionare DisposalExceptions.

1

Tenermi aggrappati a un oggetto parzialmente costruito mi sembra pericoloso, anche se funzionasse. Non userei gli inizializzatori o un ctor per gestire questo.

E se invece si utilizza una fabbrica di oggetti (non proprio la stessa di una fabbrica di classi) per creare il proprio oggetto.

Il costruttore dell'oggetto non è responsabile della creazione degli oggetti IDisposable di cui è proprietario. Invece, la fabbrica creerebbe ogni IDisposable e chiamerebbe il costruttore sull'oggetto proprietario. La fabbrica quindi imposta i membri appropriati nell'oggetto proprietario agli oggetti monouso che sono stati creati.

pseudocodice:

 

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

In C# si usa 'utilizzando':

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

VB ha anche un utilizzando ... End Using costruire. Quando si usano questi, il metodo Dispose è garantito per essere chiamato, anche nel caso di un'eccezione. È possibile rilasciare qualsiasi risorsa creata dagli inizializzatori (o dal costruttore) nel metodo Dispose.

+3

-1 Questo non funziona se il costruttore genera un'eccezione. Il nuovo oggetto non viene mai restituito, quindi non c'è nulla su cui chiamare Dispose. – chilltemp

+0

Hmmmm ... arrestato. –

12

È necessario rilevare eventuali eccezioni nel costruttore, quindi eliminare gli oggetti figlio, quindi ripetere l'eccezione originale (o una nuova eccezione che fornisce informazioni aggiuntive).

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

C'è un modo per rilevare le eccezioni negli inizializzatori (ad esempio "Font myFont = New Font (" Arial ", ... qualunque cosa ...);")? In molti casi, è molto più conveniente creare oggetti quando vengono definiti piuttosto che definirli in un unico posto e quindi crearli altrove. – supercat

+1

@supercat: non che ne sia a conoscenza. Sono d'accordo con la semplicità che cerchi, ma in questo caso essere più robusti è più importante. – chilltemp

+0

+1 ma preferisco * non * chiamare 'Dispose' nel costruttore, perché se si implementa il cosiddetto schema canonico' IDisposable' allora si potrebbe finire con una chiamata virtuale durante la chiamata del costruttore (tramite 'protected void Dispose (bool eliminando) '). –

0

Per quanto strano possa sembrare, ma sembra che GC stia ancora chiamando distruttore per oggetti IDisposable, anche se lanciano un'eccezione nel costruttore! :)

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

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

Se tu fossi abbastanza intelligente da utilizzare ad esempio da MSDN, dove hanno chiamato Dispose (false) da distruttore - pure - appena fallito! :)

+2

Alcuni oggetti IDisposable si comporteranno in modo accettabile anche se non sono disposti, perché un finalizzatore si attiverà in modo accettabile e tempestivo per pulirli. Esistono altre situazioni, tuttavia, l'impossibilità di smaltire correttamente un oggetto può causare una perdita di memoria importante di durata indefinita (ad esempio un sottoscrittore di eventi che ha riferimenti a molti altri oggetti e si abbona a un evento da un oggetto di lunga durata utilizza Dispose per annullare l'iscrizione a quell'evento. Se dispose non sparisce, né quell'oggetto, né alcuno al quale detiene un riferimento, può essere raccolto fino a quando l'oggetto a vita lunga non muore.) – supercat

+0

In vb.net, ho trovato un modello per oggetti usa e getta che consente la dichiarazione, l'inizializzazione e la pulizia dell'oggetto, da gestire su una singola riga (vedere sotto). Richiede che gli inizializzatori di campo vengano eseguiti dopo il costruttore di base, tuttavia, e quindi non è probabilmente adattabile a C#. – supercat