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.

 

  1: ' Zorg dat C:\MijnFolder\MijnFile.txt aanwezig is.
  2: Dim pad As String = "C:\MijnFolder\MijnFile.txt"
  3: ' Open de file.
  4: Dim writer1 As New StreamWriter(pad)
  5: ' Schrijf naar de file.
  6: writer1.WriteLine("Dit is een test.")
  7: writer1.Flush
  8: 
  9: ' De volgende code is niet mogelijk, de file is nog
 10: ' in gebruik en er kan niet naar geschreven worden.
 11: Dim writer2 As New StreamWriter(pad)
 12: writer2.WriteLine("Dit is een test.")
 13: writer2.Flush

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

  1: ' Zorg dat C:\MijnFolder\MijnFile.txt aanwezig is.
  2: Dim pad As String = "C:\MijnFolder\MijnFile.txt"
  3: Dim writer1 As New StreamWriter(pad)
  4: writer1.WriteLine("Dit is een test.")
  5: writer1.Flush
  6: ' Geef de file vrij voor anderen.
  7: writer1.Dispose
  8: 
  9: ' Nu gaat het wel goed!
 10: Dim writer2 As New StreamWriter(pad)
 11: writer2.WriteLine("Dit is een test.")
 12: writer2.Flush
 13: 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:

  1: Using writer1 As New StreamWriter(pad)
  2:    writer1.WriteLine("Dit is een test.")
  3:    writer1.Flush
  4: 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.

 

  1: Using conn As New SqlConnection(connString)
  2:    Dim waarde As String = "Hallo"
  3: End Using
  4: ' Het volgende is niet mogelijk.
  5: ' 'waarde' is niet meer toegangkelijk (out of scope).
  6: waarde = "Doei"

Er kunnen meerdere objecten met één Using statement gedeclareerd worden.
 

  1: Using conn As New SqlConnection(connString), cmd As New SqlCommand(query, conn)
  2:    ' Code...
  3: 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.

 

  1: Public Class Test
  2:    Implements IDisposable
  3: 
  4: #Region "IDisposable Support"
  5:    Private disposedValue As Boolean ' To detect redundant calls
  6: 
  7:    ' IDisposable
  8:    Protected Overridable Sub Dispose(disposing As Boolean)
  9:    If Not Me.disposedValue Then
 10:       If disposing Then
 11:          ' TODO: dispose managed state (managed objects).
 12:       End If
 13: 
 14:       ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
 15:       ' TODO: set large fields to null.
 16:    End If
 17:    Me.disposedValue = True
 18: End Sub
 19: 
 20: ' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources.
 21: 'Protected Overrides Sub Finalize()
 22: '   ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
 23: '   Dispose(False)
 24: '   MyBase.Finalize()
 25: 'End Sub
 26: 
 27: ' This code added by Visual Basic to correctly implement the disposable pattern.
 28: Public Sub Dispose() Implements IDisposable.Dispose
 29:    ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
 30:    Dispose(True)
 31:    GC.SuppressFinalize(Me)
 32: End Sub
 33: #End Region
 34: 
 35: 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.
 

  1: Public Class Test
  2:    Implements IDisposable
  3: 
  4:    Private disposedValue As Boolean
  5: 
  6:    Protected Overridable Sub Dispose(disposing As Boolean)
  7:       If Not Me.disposedValue Then
  8:          If disposing Then
  9:             ' Hier komt code.
 10:          End If
 11:       End If
 12:       Me.disposedValue = True
 13:    End Sub
 14: 
 15:    Public Sub Dispose() Implements IDisposable.Dispose
 16:       Dispose(True)
 17:       GC.SuppressFinalize(Me)
 18:    End Sub
 19: 
 20: End Class

Dat 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.

 

  1: Public Class Test
  2:    Implements IDisposable
  3: 
  4:    Private _connection As SqlConnection
  5:    Private _cmd As SqlCommand
  6:    Private _adapter As SqlAdapter
  7: 
  8:    Private disposedValue As Boolean
  9: 
 10:    Protected Overridable Sub Dispose(disposing As Boolean)
 11:       If Not Me.disposedValue Then
 12:          If disposing Then
 13:             If _connection IsNot Nothing Then
 14:                _connection.Dispose
 15:                _connection = Nothing
 16:             End If
 17:             If _cmd IsNot Nothing Then
 18:                _cmd.Dispose
 19:                _cmd = Nothing
 20:             End If
 21:             If _adapter IsNot Nothing Then
 22:                _adapter.Dispose
 23:                _adapter = Nothing
 24:             End If
 25:          End If
 26:       End If
 27:       Me.disposedValue = True
 28:    End Sub
 29: 
 30:    Public Sub Dispose() Implements IDisposable.Dispose
 31:       Dispose(True)
 32:       GC.SuppressFinalize(Me)
 33:    End Sub
 34: 
 35: 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.

 

  1: Sub Main()
  2:    ' Zorg dat C:\MijnFolder\MijnFile.txt aanwezig is.
  3:    Dim pad As String = "C:\MijnFolder\MijnFile.txt"
  4:    SchrijfNaarFile(pad)
  5:    GC.Collect()
  6:    GC.WaitForPendingFinalizers()
  7:    SchrijfNaarFile(pad)
  8: End Sub
  9: 
 10: Private Sub SchrijfNaarFile(ByVal pad As String)
 11:    Dim writer As New StreamWriter(pad)
 12:    writer.WriteLine("Dit is een test.")
 13:    writer.Flush()
 14: 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!