Is there any nice pattern in .Net for ensuring that IDisposable
fields owned by an object will get disposed if an exception is thrown during construction, possibly during a field initializer? The only way to surround field initializers in a Try/Catch block is if the block is outside the call to the constructor, which will make it rather difficult for cleanup code to properly dispose of anything.
The only approach I can figure would be to the object inherit from a base class whose constructor takes something like an array of IDisposable
, and sets the first item in that array to point to itself. All constructors the descendant classes should be Private or Protected, and include that parameter. Instantiation should be via factory methods, which will declare an array of one IDisposable
and pass it to the appropriate constructor. If the constructor fails, the factory method will have a reference to the partially-constructed object, which it can then dispose (the dispose method must, of course, be prepared to accept the possibility that the object may not be fully constructed).
The approach could be extended by having the object keep a list of iDisposable objects it creates, to allow the objects to be cleaned up without having to explicitly dispose each one; such a list would be useful in conjunction with the factory-method-calls-dispose approach, but is largely orthogonal to it.
Any thoughts?
You should catch any exceptions in the constructor, then dispose of your child objects, then rethrow the original exception (or a new exception that provides additional information).
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;
}
}
}
Holding on to a partially constructed object sounds dangerous to me, if it would even work. I wouldn't use initializers or a ctor to handle this.
How about if instead, you use an object factory (not quite the same as a class factory) to create your object.
The constructor of your object would not be responsible for creating the IDisposable objects that it owns. Instead, the factory would create each IDisposable and it would call the constructor on your owner object. The factory would then set the appropriate members in the owner object to the disposable objects that were created.
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??
}
I've come up with a pattern that seems pretty good. It's inspired by an someone posted on CodeProject.com--using a list to keep track of disposables; raiiBase(of T) is a base class suitable for any class whose constructor takes a single parameter. The class constructor must be protected, and construction must be done via factory method. The static makeRaii() constructor takes a delegate to a factory function, which must accept a Stack(of iDisposable) along with a parameter of the class's expected type. A sample usage:
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
Since raiiTest inherits raiiBase(of String), to create a class instance, call newRaiiTest with a string parameter. RAII() is a generic function that will register its argument as an iDisposable that will need cleaning up, and then return it. All registered disposables will be Disposed when either Dispose is called on the main object, or when an exception is thrown in the construction of the main object.
Here's the riaaBase class:
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
Note that a custom exception type will be thrown if disposal fails for any or all of the registered disposable objects. InnerException will indicate whether the constructor failed; to see which disposer(s) failed, check DisposalExceptions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With