2010-09-29 10 views
6

Gibt es ein nettes Muster in .NET, um sicherzustellen, dass iDisposable-Felder, die einem Objekt gehören, entsorgt werden, wenn während der Erstellung, möglicherweise während eines Feldinitialisierers, eine Ausnahme ausgelöst wird? Die einzige Möglichkeit, Feldinitialisierer in einem Try/Catch-Block zu umschließen, besteht darin, dass der Block außerhalb des Aufrufs des Konstruktors liegt, was es für den Bereinigungscode ziemlich schwierig macht, etwas ordnungsgemäß zu beseitigen. Der einzige Ansatz, den ich mir vorstellen kann, wäre, dass das Objekt von einer Basisklasse erbt, deren Konstruktor etwas wie ein Array von iDisposable nimmt, und setzt das erste Element in diesem Array auf sich selbst. Alle Konstruktoren, die abgeleiteten Klassen sollten Privat oder Orotected sein und diesen Parameter einschließen. Instanziierung sollte über Factory-Methoden erfolgen, die ein Array von einem iDisposable deklarieren und an den entsprechenden Konstruktor übergeben. Wenn der Konstruktor fehlschlägt, hat die Factory-Methode einen Verweis auf das teilweise konstruierte Objekt, das sie dann entsorgen kann (die dispose-Methode muss natürlich bereit sein, die Möglichkeit zu akzeptieren, dass das Objekt möglicherweise nicht vollständig aufgebaut ist).Umgang mit iDisposable in fehlgeschlagenen Initialisierer oder Konstruktor

Der Ansatz könnte erweitert werden, indem das Objekt eine Liste der erstellten iDisposable-Objekte beibehält, damit die Objekte bereinigt werden können, ohne jedes Objekt explizit ablegen zu müssen; Eine solche Liste wäre in Verbindung mit dem Ansatz "Factory-Methode - Aufrufe - Verfügen" nützlich, ist jedoch weitgehend orthogonal zu ihr.

Irgendwelche Gedanken?

Antwort

1

Ich habe ein Muster gefunden, das ziemlich gut aussieht. Es wurde von einer Person inspiriert, die auf CodeProject.com veröffentlicht wurde - mithilfe einer Liste, um Verbrauchsmaterialien zu verwalten. RaiiBase (von T) ist eine Basisklasse, die für jede Klasse geeignet ist, deren Konstruktor einen einzigen Parameter annimmt. Der Klassenkonstruktor muss geschützt werden, und die Konstruktion muss über die Factory-Methode erfolgen. Der statische makeRaii() -Konstruktor nimmt einen Delegaten zu einer Factory-Funktion, die einen Stack (von iDisposable) zusammen mit einem Parameter des erwarteten Typs der Klasse akzeptieren muss.Eine Probe Nutzung:

 
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 

Seit raiiTest erbt raiiBase (von String), eine Instanz der Klasse, rufen newRaiiTest mit einem String-Parameter zu erstellen. RAII() ist eine generische Funktion, die ihr Argument als iDisposable registriert, das aufgeräumt werden muss, und es dann zurückgibt. Alle registrierten Einwegartikel werden entsorgt, wenn entweder Dispose für das Hauptobjekt aufgerufen wird oder wenn eine Ausnahme in der Konstruktion des Hauptobjekts ausgelöst wird.

Hier ist die riaaBase Klasse:

 
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 

Hinweis, dass ein benutzerdefinierter Ausnahmetyp ausgelöst wird, wenn zur Verfügung für einen oder alle der registrierten Wegwerfobjekte ausfällt. InnerException zeigt an, ob der Konstruktor fehlgeschlagen ist. Um zu sehen, welcher (n) Entsorger (n) gescheitert ist (n), prüfen Sie DisposalExceptions.

1

Das Festhalten an einem teilweise konstruierten Objekt klingt für mich gefährlich, wenn es überhaupt funktionieren würde. Ich würde keine Initialisierer oder einen Ctor verwenden, um damit umzugehen.

Wie wäre es, wenn Sie stattdessen eine Objektfactory (nicht ganz wie eine Klassenfabrik) verwenden, um Ihr Objekt zu erstellen.

Der Konstruktor Ihres Objekts ist nicht für die Erstellung der IDisposable-Objekte verantwortlich, die ihm gehören. Stattdessen würde die Factory jedes IDisposable erstellen und den Konstruktor auf Ihrem Besitzerobjekt aufrufen. Die Factory würde dann die entsprechenden Elemente im Eigentümerobjekt auf die Einmalobjekte setzen, die erstellt wurden.

Pseudo-Code:

 

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# werden Sie von 'verwenden':

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

VB hat auch eine Verwendung ... End Using konstruieren. Wenn Sie diese verwenden, wird die Dispose-Methode garantiert auch im Falle einer Ausnahme aufgerufen. Sie können alle Ressourcen freigeben, die von den Initialisierern (oder dem Konstruktor) in der Dispose-Methode erstellt wurden.

+3

-1 Dies funktioniert nicht, wenn der Konstruktor eine Ausnahme auslöst. Das neue Objekt wird nie zurückgegeben, sodass Dispose nicht aufgerufen werden kann. – chilltemp

+0

Hmmmm ... gesprengt. –

12

Sie sollten alle Ausnahmen im Konstruktor abfangen, dann Ihre untergeordneten Objekte verwerfen und dann die ursprüngliche Ausnahme (oder eine neue Ausnahme, die zusätzliche Informationen bereitstellt) erneut auslösen.

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

Gibt es eine Möglichkeit, Ausnahmen in Initialisierungen abzufangen (z. B. "Schriftart myFont = Neue Schriftart (" Arial ", ... was auch immer ...);")? In vielen Fällen ist es viel bequemer, Objekte zu erstellen, wenn sie definiert sind, als sie an einer Stelle zu definieren und sie dann woanders zu erstellen. – supercat

+1

@supercat: Nicht dass ich mir dessen bewusst bin. Ich stimme der Einfachheit zu, die Sie suchen, aber in diesem Fall ist Robustheit wichtiger. – chilltemp

+0

+1, aber ich bevorzuge * nicht * im Konstruktor 'Dispose' zu ​​nennen, denn wenn Sie das sogenannte kanonische' IDisposable'-Muster implementieren, könnten Sie während des Konstruktoraufrufs einen virtuellen Aufruf erhalten (über 'protected void Dispose (bool entsorgt) '). –

0

So seltsam wie es scheinen mag, aber es sieht aus, dass GC noch Destruktor für IDisposable-Objekte aufruft, selbst wenn sie Ausnahme im Konstruktor auslösen! :)

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

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

Wenn Sie klug genug sind, beispielsweise von Msdn zu verwenden, wo sie Dispose (false) aus destructor genannt - gut - man muss nur failed! :)

+2

Einige IDisposable-Objekte verhalten sich akzeptabel, selbst wenn sie nicht entsorgt werden, da ein Finalizer in akzeptabler Zeit feuern wird, um sie zu bereinigen. Es gibt jedoch andere Situationen, in denen ein Fehler bei der ordnungsgemäßen Entsorgung eines Objekts zu einem großen Speicherverlust von unbestimmter Dauer führen kann (z. B. ein Ereignisteilnehmer, der auf viele andere Objekte verweist und ein Ereignis von einem langlebigen Objekt abonniert, verwendet Dispose to Dieses Ereignis wird abbestellt. Wenn dispose nicht ausgelöst wird, können weder das Objekt noch Objekte, auf die es verweist, gesammelt werden, bis das langlebige Objekt stirbt. – supercat

+0

In vb.net habe ich ein Muster entwickelt für Einwegobjekte, die die Deklaration, Initialisierung und Bereinigung von Objekten ermöglichen, die in einer einzigen Zeile behandelt werden (siehe unten). Es erfordert jedoch, dass Feldinitialisierer nach dem Basiskonstruktor ausgeführt werden und daher wahrscheinlich nicht für C# angepasst werden können. – supercat

Verwandte Themen