Het Disposable-pattern

 
27 september 2011

Iedereen weet dat als jouw object belangrijke resources gebruikt, dat je dan het Disposable-pattern moet implementeren, zodat gebruikers van jouw object zelf kunnen aangeven dat ze NU klaar zijn met het gebruik, en die resources dus weer vrijgegeven kunnen worden. Maar in de praktijk blijft het bij de abstracte kennis dat je dan IDisposable moet implementeren, en dus Dispose(), en dat er een standaard-patroon is waarin ook een Dispose(bool disposing) komt kijken – maar hoe zat het ook al weer precies, en waarom doen we het op die manier?

Het standaard-patroon ziet er zo uit:

class MyClass : IDisposable {
  bool disposed = false;
  IDisposable someManagedResource;
  IntPtr anUnmanagedResource;
 
  // ...regular code...
 
  public void Dispose() {
    this.Dispose(true);
    GC.SuppressFinalize(this);
  }
 
  protected virtual void Dispose(bool disposing) {
    if (disposed) { return; }
 
    // release unmanaged resources, e.g.:
    this.release(anUnmanagedResource);
 
    if (disposing) {
      // release managed resources, e.g.:
      someManagedResource.Dispose();
    }
 
    disposed = true;
  }
 
  ~MyClass() {
    Dispose(false);
  }
}

Waar komt deze constructie vandaan?

Destructor

Zoals gezegd, het idee achter IDisposable is dat jouw object resources die het in beheer heeft, op een nette manier kan vrijgeven. Dat kan door de Dispose()-methode aan te roepen, ofwel het object in een using-constructie te vatten (wat automatisch zorgt voor een Dispose()-aanroep). Je zou denken dat je dan in die aanroep gewoon je spullen kunt opruimen, en klaar is Kees. Maar zoals je ziet bevat het patroon hierboven veel meer dan alleen die Dispose()-methode: er is ook nog een overload Dispose(bool) en een destructor ~MyClass. En er is vast een reden waarom die er zijn.

Die laatste is eenvoudig te verklaren: je zal namelijk altijd zien dat die klungels die jouw object gebruiken vergeten om Dispose() aan te roepen (of te lui zijn, of last van bugs hebben, of een goeie reden denken te hebben, of …), zodat je databaseverbinding gewoon open blijft staan, die RegistryKey gelockt blijft, dat bestand op schijf open blijft staan, enzovoort.

De destructor is de tegenhanger van een constructor, en wordt automatisch door de CLR aangeroepen op het moment dat je object uit scope is geraakt en de garbage collector langskomt. Dat gebeurt bijvoorbeeld als je tijdelijk krap in je geheugen komt te zitten, of expliciet GC.Collect() aanroept, oftewel: met wat geluk gebeurt dat zelfs al vóórdat je programma afsluit. En dat geeft je dus een extra kans om je resources netjes vrij te geven in plaats van dat het systeem zelf maar moet uitzoeken wat ermee moet gebeuren.

Blijft nog de vraag waarom het dan zo ingewikkeld moet, met een overload enzo. Waarom niet gewoon in zowel Dispose() als in de destructor dezelfde code gebruiken die alles opruimt? Waarom niet gewoon zo:

class MyClass : IDisposable {
  bool disposed = false;
  IDisposable someManagedResource;
  IntPtr anUnmanagedResource;
 
  // ...regular code...
 
  public void Dispose() {
    if (disposed) { return; }
    this.release(anUnmanagedResource);
    someManagedResource.Dispose();
    disposed = true;
  }
 
  ~MyClass() {
    Dispose(); // do not try this at home! Lees verder...
  }
}

…? Maar ook daar zit een goeie reden achter, waar ik zo op kom. Eerst even wat extra achtergrond-informatie.

Gemanagede en ongemanagede resources

Resources zijn er in twee varianten: een ‘rauwe’ unmanaged resource, zoals de IntPtr hierboven, en gewrapte resources, waarbij de wrapper een normaal object is dat zelf IDisposable implementeert omdat het zelf ook weer resources (rauw of gewrapt) beheert. Het toverwoord bij het Disposable-pattern is daarbij ‘beheert’: we gaan ervan uit dat jouw object de baas is over de gebruiksduur van de resource, en dus zelf kan besluiten wanneer het ding wordt vrijgegeven. (Als je dus een referentie naar je resource weggeeft, moet je goed weten wat ermee gaat gebeuren en wanneer het ding niet meer gebruikt wordt. Of anders moet die externe code ertegen kunnen dat het bestand halverwege gesloten wordt, de database-verbinding uitvalt, enz.)

Het meest verwarrende aan dit patroon vind ikzelf persoonlijk de standaardnaamgeving van het argument disposing in de regel protected void Dispose(bool disposing). Overal en altijd wordt (terecht) gehamerd op het belang van goede naamgeving, maar net in dit geval waar het toch effe lastig is, nemen we zo’n nietszeggende standaardnaam. Het had beter iets van managedAlso of includeWrappedResources ofzo kunnen heten.

Want kijk maar wat er gebeurt: de ongemanagede resources worden sowieso opgeschoond, en de disposing-parameter bepaalt of de gemanagede resources ook opgeruimd worden. Dat is het enige waar hij voor wordt gebruikt. Deze overload is geen deel van de interface, en wordt ook niet ergens impliciet aangeroepen (hij hoeft nl. niet eens te bestaan of zo te heten, en is niet geannoteerd, dus met reflectie kom je ook nergens); en hij is protected, dus van de buitenkant kan je hem ook niet expliciet aanroepen. De enige twee keren dat hij wordt gebruikt is in de methode Dispose() en in de destructor ~MyClass.

Leuk natuurlijk dat die parameter er is, maar waarom maken we überhaupt dat onderscheid tussen wel of niet de gemanagede resources opruimen? Waarom, nogmaals, niet die eenvoudige variant met een destructor die simpelweg Dispose() aanroept? Dat heeft dan weer te maken met de levenscyclus van je object te maken.

Levenscyclus

Zoals je ziet wordt de overload vanuit de normale Dispose()-methode aangeroepen met true als argument: je bent hier nog in je normale programma-flow bezig, alle objectverwijzingen die je hebt zijn nog geldig, waaronder die naar je gemanagede resources. Daar kan je nu zelf ook Dispose() tegen zeggen zodat zij ook weten dat ze hun eigen resources kunnen gaan opruimen. Als je dat niet doet dan weten ze van niks, en gebeurt er dus ook niks, en krijgen ze pas een seintje (via hun eigen destructor) als de garbage collector langskomt – en worden hun resources dus ook weer onnodig lang vastgehouden.

Als je in de destructor terecht komt, wordt daarentegen de overload met argument false aangeroepen. Nu gelden er namelijk andere regels: je komt hier niet via normale programma-flow; de CLR is nu zelf het geheugen aan het opschonen, en ongebruikte objecten zijn of worden weggegooid. Dat wil dus zeggen dat de referenties naar die gemanagede resources nu mogelijk verwijzen naar objecten die al niet meer bestaan (omdat de GC ze al heeft opgeruimd), en dus de lege ruimte in wijzen. Je kan dus niet meer Dispose() tegen die objecten zeggen: dat zou namelijk een dikke exceptie opleveren, en een exceptie die uit jouw deconstructor ontsnapt betekent het einde van je hele applicatie zonder kans op recovery – niet doen dus. Je ongemanagede resources daarentegen kan je nog rustig opruimen: juist doordat ze ongemanaged zijn betekent dat de CLR er niet zomaar ongevraagd mee aan de haal gaat.

Het feit dat je tegen je gemanagede resources geen Dispose() meer kan zeggen is overigens geen probleem, aangezien jouw object deze resources beheert. Jouw object wordt op dit moment ge-garbage-collect, dus zijn levensduur is in feite voorbij – en dus ook die van de beheerde resources. Dus als het goed is zijn er nergens meer levende verwijzingen naar die resources, en dat betekent dat die resources ook ge-garbage-collect zullen worden. Hun eigen destructor runt dus ook, waardoor zij hun eigen ongemanagede resources zullen opruimen, terwijl hun gemanagede resources via een mooi domino-effect ook weer ge-garbage-collect zullen worden, enzovoort.

Sanity check en stroomlijning

Natuurlijk hoef je je resources maar een enkele keer op te ruimen, en daarna is je object in principe ook niet meer bruikbaar. Daarom houden we in het field disposed bij of dat al gebeurd is. Als dan Dispose() nog een keer aangeroepen wordt, hoeft hij geen dubbel werk te doen – als dat niet sowieso al een exceptie oplevert. Daarnaast kan je in je business-methoden dit field checken om te zien of je resources nog wel beschikbaar zijn om de methode succesvol uit te kunnen voeren – of anders een ObjectDisposedException te gooien.

En verder, als Dispose() al een keer expliciet is aangeroepen dan zijn alle resources al opgeruimd, en hoeft dat niet nog eens in de destructor te gebeuren. We kunnen dus tegen de garbage collector zeggen dat die t.z.t. niet hoeft te worden aangeroepen – vandaar de aanroep naar GC.SuppressFinalize(this) in de Dispose()-methode. Dit zorgt er bovendien voor dat je object veel efficiënter wordt opgeruimd.

Tot slot geeft dit patroon al een aanwijzing over hoe je omgaat met subclassing van MyClass: omdat de Dispose() niet virtual is, zit je subklasse aan deze implementatie vast – maar de overload Dispose(bool) is protected virtual. Daar kan je dus wel wat mee: zoals de resources vrijgeven die specifiek voor die subklasse zijn. Dat gaat dan volgens dezelfde principes als voor de basisklasse: altijd je eigen (subklasse-specifieke) ongemanagede resources opruimen, en je gemanagede resources alleen als de parameter true is. En natuurlijk moet je niet vergeten dan aan het eind van je override ook nog even base.Dispose(disposing) aan te roepen (met hetzelfde disposing-argument als jijzelf hebt binnengekregen), zodat je basisklasse ook de gelegenheid heeft om zijn eigen spullen vrij te geven. En daarmee heb je de opruimwerkzaamheden al aardig onder de duim.

Lees verder…

Over het Disposable-pattern is op internet al al meer gezegd en ook gevraagd dus daar is genoeg over te lezen. Maar hopelijk helpt deze blog ook al wat.


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Development

Tags: , , , ,