Opruimen met IDisposable

Dit is een reeds eerder gepubliceerde post van de vorige VBcentral website door Sander Rossel.
Veel .NET programmeurs onderschatten het belang van het opruimen van resources en het vrijgeven van geheugen. Het is waar dat .NET veel van dit soort dingen voor je regelt, maar dat wil niet zeggen dat de programmeur zelf geen verantwoordelijkheden heeft. 
Voorbeelden van resources kunnen geopende bestanden of connecties naar een database zijn. Deze mogen per definitie niet langer gebruikt worden dan nodig omdat dit het systeem belast of het voor anderen onmogelijk maakt om deze data te benaderen. 
Stel dat je text naar een .txt bestand schrijft, maar niet de toegang tot de file vrijgeeft. Iedere volgende call naar de functie gooit nu een Exception omdat de file nog geopend is. 

 
' Zorg dat C:\MijnFolder\MijnFile.txt aanwezig is. 
Dim pad As String = "C:\MijnFolder\MijnFile.txt" 
 
' Open de file. 
Dim writer1 As New StreamWriter(pad) 
 
' Schrijf naar de file. 
writer1.WriteLine("Dit is een test.") 
writer1.Flush 
 
' De volgende code is niet mogelijk, de file is nog 
' in gebruik en er kan niet naar geschreven worden. 
Dim writer2 As New StreamWriter(pad) 
writer2.WriteLine("Dit is een test.") 
writer2.Flush 


Gelukkig is dit probleem makkelijk te verhelpen. Kijk eens naar de herschreven code. 

 
' Zorg dat C:\MijnFolder\MijnFile.txt aanwezig is. 
 
Dim pad As String = "C:\MijnFolder\MijnFile.txt" 
Dim writer1 As New StreamWriter(pad) 
 
writer1.WriteLine("Dit is een test.") 
writer1.Flush 
 
' Geef de file vrij voor anderen. 
writer1.Dispose 
 
' Nu gaat het wel goed! 
Dim writer2 As New StreamWriter(pad) 
 
writer2.WriteLine("Dit is een test.") 
writer2.Flush 
writer2.Dispose 


Wat is de truc? De Dispose functie. Omdat een StreamWriter object intern system resources gebruikt (in dit geval een file handle) moeten deze vrij gemaakt worden. Dit is precies wat de Dispose functie doet. Die Dispose functie komt niet zomaar uit de lucht vallen, StreamWriter implementeert namelijk de IDisposable Interface waar de Dispose functie in staat gedefinïeerd. 
De regel is dat wanneer een object IDisposable implementeert dat deze ALTIJD aangeroepen moet worden voordat dit object 'out of scope' gaat, dat wil zeggen dat je er niet meer bij kan met je code. Dit betekent dus dat je goed op moet letten, want ook als er Exceptions optreden moet Dispose aangeroepen worden! 
Gelukkig bestaat hier een klein trucje voor, namelijk het Using keyword. 
De bovenstaande code had herschreven kunnen worden als: 

 
Using writer1 As New StreamWriter(pad) 
 
   writer1.WriteLine("Dit is een test.") 
   writer1.Flush 
 
End Using 


Met een using block wordt een object altijd d.m.v. Dispose opgeruimd, ook als er Exceptions optreden. 
Let op dat alles wat in een Using block staat daarbuiten niet toegangkelijk is. 
 
 
Using conn As New SqlConnection(connString) 
 
   Dim waarde As String = "Hallo" 
 
End Using 
 
' Het volgende is niet mogelijk. 
' 'waarde' is niet meer toegangkelijk (out of scope). 
 
waarde = "Doei" 


Er kunnen meerdere objecten met één Using statement gedeclareerd worden. 
 
 
Using conn As New SqlConnection(connString), cmd As New SqlCommand(query, conn) 
 
   ' Code... 
 
End Using 


U kunt zich voorstellen dat het in bepaalde gevallen nodig is om zelf IDisposable te moeten implementeren. Bijvoorbeeld wanneer uw klasse variabelen heeft die opgeruimd moeten worden. IDisposable is zo belangrijk dat wanneer je hem zelf implementeert Microsoft je een halve implementatie cadeau geeft! 
Kijk maar eens naar de code die gegenereerd wordt als je IDisposable zelf implementeert in een klasse. 
 
 
Public Class Test 
 
   Implements IDisposable 
 
#Region "IDisposable Support" 
 
   Private disposedValue As Boolean ' To detect redundant calls 
 
   ' IDisposable 
   Protected Overridable Sub Dispose(disposing As Boolean) 
 
   If Not Me.disposedValue Then 
 
      If disposing Then 
         ' TODO: dispose managed state (managed objects). 
      End If 
 
      ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below. 
      ' TODO: set large fields to null. 
 
   End If 
 
   Me.disposedValue = True 
 
End Sub 
 
' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources. 
'Protected Overrides Sub Finalize() 
 
'   ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above. 
'   Dispose(False) 
 
'   MyBase.Finalize() 
 
'End Sub 
 
' 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 


Dat is niet niks! Maar laat het u niet afschrikken. Lees eerst de code (en commentaar) eens goed door. Laten we al het commentaar weghalen en er blijft vrij weinig over. 
 
 
Public Class Test 
 
   Implements IDisposable 
 
   Private disposedValue As Boolean 
 
   Protected Overridable Sub Dispose(disposing As Boolean) 
 
      If Not Me.disposedValue Then 
 
         If disposing Then 
 
            ' Hier komt code. 
 
         End If 
 
      End If 
 
      Me.disposedValue = True 
 
   End Sub 
 
   Public Sub Dispose() Implements IDisposable.Dispose 
 
      Dispose(True) 
 
      GC.SuppressFinalize(Me) 
 
   End Sub 
 
End Class 


Ziet er al een stuk behapbaarder uit. De IDisposable.Dispose() is al geïmplementeerd en hier moeten we dus vanaf blijven. Waar komt de code dan wel? Ik heb een klein stukje commentaar toegevoegd waar u in vrijwel de meeste gevallen uw code gaat schrijven. 
Laten we eens naar een voorbeeldje kijken waar een klasse variabelen heeft om queries uit te voeren op een database. Omdat de velden Disposable zijn is het nodig om de klasse IDisposable te laten implementeren. 
 
 
Public Class Test 
 
   Implements IDisposable 
 
   Private _connection As SqlConnection 
   Private _cmd As SqlCommand 
   Private _adapter As SqlAdapter 
   Private disposedValue As Boolean 
 
   Protected Overridable Sub Dispose(disposing As Boolean) 
 
      If Not Me.disposedValue Then 
 
         If disposing Then 
 
            If _connection IsNot Nothing Then 
               _connection.Dispose 
               _connection = Nothing 
            End If 
 
            If _cmd IsNot Nothing Then 
               _cmd.Dispose 
               _cmd = Nothing 
            End If 
 
            If _adapter IsNot Nothing Then 
               _adapter.Dispose 
               _adapter = Nothing 
            End If 
 
         End If 
 
      End If 
 
      Me.disposedValue = True 
 
   End Sub 
 
   Public Sub Dispose() Implements IDisposable.Dispose 
 
      Dispose(True) 
 
      GC.SuppressFinalize(Me) 
 
   End Sub 
 
End Class 


U ziet dat ik eerst kijk of de objecten die opgeruimd moeten worden wel zijn geinstantieerd. Dit is nodig omdat er tijdens de instantiatie bijvoorbeeld een Exception heeft op kunnen treden waardoor de variabelen geen waarde hebben en een NullReferenceException zou op kunnen treden. Ook ken ik expliciet de 'waarde' Nothing toe aan de variabelen. Hierdoor kan de Garbage Collector het geheugen direct vrij maken (meestal zal dat echter pas later gebeuren).

Stop! Wat is die Garbage Collector dan? Dit is iets wat op de achtergrond draait en geheugen vrij maakt als dat nodig is. U ziet in de Dispose functie een GC.SuppressFinalize(Me) staan. GC staat voor Garbage Collector. Ik raad u sterk aan u hier niet mee bezig te houden. Rommelen met de GC maakt vaak meer kapot dan dat het goed maakt. De GC doet zijn werk het best als u het zijn gang laat gaan. Overigens ruimt de GC enkel reference types op (en doet dit pas wanneer nodig). Value types (zoals integers, decimals, objecten die overerven van ValueType) worden opgeruimd zodra ze ‘out of scope’ gaan.

Wat doet dan die GC.SuppressFinalize(Me)? U zag in de originele code die gegenereerd werd door IDisposable te implementeren dat u een Finalize functie kunt overriden. De Finalizer wordt aangeroepen door de GC. Als u echter de Dispose functie goed implementeert en deze wordt aangeroepen dan zal de Finalizer overbodig worden. GC.SuppressFinalize(Me) zorgt er voor dat de Finalizer in dat geval niet aangeroepen wordt. De Finalizer kan er dan voor zorgen dat bepaalde system resources alsnog worden vrijgegeven (zodat u na een tijdje alsnog naar een file kan schrijven ook al is de StreamWriter niet gedisposed). 
Om dit te bewijzen zullen we het eerste stukje code nogmaals uitvoeren, maar ditmaal forceren we de GC om te 'Finalizen'. Het stukje code met de StreamWriter is verplaatst naar een aparte functie zodat de StreamWriter 'out of scope' gaat en hij gecollect kan worden door de GC. LET OP! Gebruik dit niet in productie code, de GC doet zijn werk het beste als je er niet tussen komt. Beter is het om te disposen, zoals hierboven staat beschreven. 
 
 
Sub Main() 
 
   ' Zorg dat C:\MijnFolder\MijnFile.txt aanwezig is. 
 
   Dim pad As String = "C:\MijnFolder\MijnFile.txt" 
 
   SchrijfNaarFile(pad) 
 
   GC.Collect() 
   GC.WaitForPendingFinalizers() 
 
   SchrijfNaarFile(pad) 
 
End Sub 
 
Private Sub SchrijfNaarFile(ByVal pad As String) 
 
   Dim writer As New StreamWriter(pad) 
 
   writer.WriteLine("Dit is een test.") 
 
   writer.Flush() 
 
End Sub 


Een tweede call naar SchrijfNaarFile is mogelijk omdat de filehandle is vrijgegeven in de Finalizer. 
U zult vooral met de Finalize method te maken krijgen als u met unmanaged code aan de gang gaat. Hier zitten echter de nodige haken en ogen aan die ik hier niet ga bespreken omdat u het waarschijnlijk niet nodig gaat hebben. Een belangrijke 'gotcha' als u met Finalizers werkt is dat ze in willekeurige volgorde worden aangeroepen en dat u niet weet wanneer ze worden aangeroepen. Geen Dispose aanroepen en op de Finalizer rekenen is dan ook geen goed idee.

Er moeten een aantal kanttekeningen geplaatst worden bij het gebruik van IDisposable en Dispose. Zo worden de meeste objecten onbruikbaar nadat Dispose is aangeroepen (als u het toch probeert zult u een ObjectDisposedException kunnen verwachten). Als u zelf IDisposable implementeert zou u zich hier aan moeten houden. Het is niet verplicht, het is mogelijk u er niet aan te houden, maar het is wel wat door veel programmeurs verwacht wordt. 
Ga niet zelf een Dispose functie schrijven, implementeer IDisposable. Zo zullen uw klassen ook door andere .NET klassen en frameworks juist behandeld worden. 
Als u twijfelt of u wel of geen Dispose aan moet roepen (in bepaalde gevallen is dit erg lastig, bijvoorbeeld met Threading) raadpleeg dan MSDN of Google.

Geheugen management is niet makkelijk! Gelukkig blijft u een hoop bespaart in VB, maar let op... Zogenaamde 'memory leaks' en het onbedoeld vasthouden of locken van resources kunnen voor vervelende problemen zorgen en zijn vaak moeilijk op te lossen. Hopelijk helpt deze blog u deze problemen te voorkomen. Succes!

Nieuwsbrief

Blijf op de hoogte van alles wat op VBcentral gebeurd. Meld je nu aan voor onze nieuwsbrief! »

Over ons

Wij zijn gek op het Microsoft .NET ontwikkelplatform en haar ontwikkeltalen, maar we hebben een sterke voorkeur voor Visual Basic .NET! »

Neem contact op

VBcentral.nl
Bataafseweg 20
7101 PA Winterswijk
Nederland
+31 (543) 538 388
info@vbcentral.nl