2008. november 8., szombat

Finalizers and the IDisposable pattern

Ha objektum-orientált módon fejlesztünk, akkor egy új objektumpéldány létrehozásához egy speciális metódust, a konstuktort fogjuk használni - akár managed, akár unmanaged környezetben tesszük ezt.
Viszont amikor a példányunkra már nincs szükség, unmanaged környezetben egy másik speciális metódus, a destruktor ("megsemmisítő") hívásával takaríthatunk ki magunk után, míg managed környezetben a szemétgyűjtő végzi el helyettünk ezt a feladatot. Kényelmes, nem?
De, viszont lehet vele egy kis bökkenő: egyrészt nem tudjuk, konkrétan mikor is fut le (magyarul: nem-determiniszikus a futás ideje), másrészt, ha nincs destruktor, akkor tejles egészében a GC-re bízzuk az általunk lefoglalt erőforrások felszabadítását, pedig néha hasznos lenne, ha a példányunk megsemmísítésekor mi magunk is eszközölhetnénk némi takarítást. Szerencsére mindkét problémára van megoldás.

C#-ban destruktorunk ugyan nincsen, de van egy speciális, Finalize nevű metódus, ami nagyon hasonló célt szolgál. Annyira hasonló, hogy a deklarálásának a móda azonos a C++-os destruktoréval, azaz ha szeretnék egy finalizer metódust az osztályunkba, akkor a ~OsztályNév() névvel kell ellátnunk, és majd a fordító csinál belőle egy Finalize() metódust.
A finalizer metódust a CLR fogja meghívni, mielőtt a már nem használt objektumpéldányunkat kisöpörné a memóriából. Ez tehát egy remek lehetőség arra, hogy az osztályunkból esetlegesen lefoglalt unmanaged erőforrásokat is felszabadítsuk.
Ezzel az első problémára, miszerint mi nem tudunk beleszólni a példány-megsemmítési folyamatba, meg is van a gyógyír. De mi a helyzet a nem-determinisztikussággal?

Lézezik egy interface, az IDisposable, ami szabad magyar fordításban kb. azt jelenti: "tőlem meg lehet szabadulni". Az IDisposable egyetlen metódus megvalósítását írja elő, a void Dispose()-ét. Ez a metódus ugyanarra szolgál - tehát nekünk azt kell benne implementálni -, amire a finalizer: fel kell benne szabadítani minden olyan erőforrást, amit a GC nem fog tudni. A Dispose, mint minden tisztességes metódus, bármikor meghívható, ezzel tehát a nem-determinisztikusság problémáját is megoldottuk: ha már nem kell az objektumpéldányunk, meghívjuk rajta a Dispose()-t, és kész.

Most mindjárt két megoldásunk is van, és körülbelül ugyanazt kéne megvalósítani a finalizerben, illetve a Dispose-ban. Ez bizony nem túl szép, szerencsére ezt is triviális megoldani: rakjuk ki egy privát metódusba a lényegi kódot, és mindkét helyről hívjunk tovább oda.
Ez már közel tökéletes, akár hátra is dőlhetnénk, de bizony még mindig van egy bibi: ha mi is meghívjuk a Dispose-t, aztán a GC is a finalizert, akkor a felszabadító kód kétszer fog lefutni (már ha tud...). Meg kéne mondani valahogy a GC-nek, hogy ha mi már dispose-oltunk "kézzel", akkor ő hanyagolja a finalizálást. Szerencsére ezt nem túl bonyolult módon közölhetjuk vele, egyszerűen a statikus GC.SuppressFinalize() meghívásával, aminek egy objektum-referenciát kell átadnunk, egész pontosan azét az objektumét, aminek a finalizálását szupresszálni szeretnénk.

Az egész fenti litániát az okosok összefoglalták egy kis kód-darabba, amit a .netes világ úgy hív: Disposable Pattern. Valahogy így néz ki:

class MyType : IDisposable
{
    ~MyType()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        // shared cleanup logic
        if (disposing)
        {
            // dispose-specific logic
            GC.SuppressFinalize(this);
        }
    }
}
Nagyon a magyarázatába nem mennék bele - akinek a fentiek alapján nem tiszta, hogy mit csinál, az álljon be juhásznak. :)

1 megjegyzés:

Robesz írta...

Ez fain, köszi ;)

Milyen egyszerűnek tűnik, mégis milyen könnyű elfelejteni. Merthogy üzleti igény legtöbbször arra van, hogy megnyomom a nagy piros gombot, akkor történjen ez-meg az, diszpozálást senki nem kér :)

Itt találtam egy kis ötletelést tredszéf diszpozálásról: http://www.csharpfriends.com/Forums/ShowPost.aspx?PostID=60743

Megjegyzés küldése