1 Hernyák Zoltán Programozási Nyelvek II. Eszterházy Károly Főiskola Számítástudományi tsz
2 A konstruktor-ok a példány használata előtt hívódnak meg, a példány életciklusának elején. A destruktorok olyan speciális metódusok, amelyek az életciklus végén hívódnak meg. A destruktorokban a példány megszűnése előtti ‘nagytakarítás’-t kell elvégezni: pluszban foglalt memória felszabadítása megnyitott file-ok lezárása nyomtatás befejezésének jelzése hálózati kapcsolatok lezárása stb…
3 A destruktorok neve kötelezően megegyezik az osztály nevével, de előtte egy ~ jel kell szerepeljen! class TFtpKliens { ~TFtpKliens() { // hálózati kapcsolatok lezárása... } class TFtpKliens { ~TFtpKliens() { // hálózati kapcsolatok lezárása... } A destruktornak nem lehet elérési szint módosítója (automatikusan public)! A destruktornak nem lehet paramétere sem! Ezért minden osztályhoz maximum 1 db destruktort készíthető!
4 Miért ez a sok megkötés a destruktorra? (#1) Történelmileg a destruktort először a programozók hívták meg explicit módon, valami hasonló módon: TVerem v = new TVerem(); … // v példány használata free v ~TVerem(); TVerem v = new TVerem(); … // v példány használata free v ~TVerem(); A destruktort hívásakor szabadítódott fel a példányhoz tartozó memóriaterület is. Ha a programozó ‘elfelejtette’ ezt meghívni, akkor a példány ottragadt a memóriában, és memóriaszivárgás (memory leak) keletkezett. Az ilyen program ha sokáig futott a memóriában, akkor elég sok idő után a teljes memóriát ilyen ‘beragadt’, haszontalan példányok foglalták el. ( ez egyébként nem működik C#-ban !!!!
Tipikus hibák: A programozó ‘elfelejti’ meghívni a felszabadítást. Ekkor a példány ottragadt a memóriában, és memóriaszivárgás (memory leak) keletkezett. Az ilyen program ha sokáig futott a memóriában, akkor elég sok idő után a teljes memóriát ilyen ‘beragadt’, haszontalan példányok foglalták el. A programozó rosszkor (idő előtt) hívta meg a felszabadítást, a példányra még van hivatkozás. a = peldany; b = a; // b is ugyanarra a példányra mutat free a; // a példány eltűnik a memóriából b.muvelet(); // ez itt hiba forrása már a = peldany; b = a; // b is ugyanarra a példányra mutat free a; // a példány eltűnik a memóriából b.muvelet(); // ez itt hiba forrása már Ekkor a program ‘bármit’ is csinálhat… de jót ritkán
6 (#2) Erre megoldást nyújtott a referencia típusú változó: Ennek során a futtató rendszer nyilvántartotta nemcsak azt, hogy hol vannak a példányok számára lefoglalt memóriaterületek, hanem azt is, hogy hány változó hivatkozik az adott példányra. ( referenciaszámlálás elve ) - Amikor egy példány memóriaterületét egy változóba értékül adjuk, akkor a referenciaszámláló növelődik 1-el. -Amikor egy ilyen változó másik értéket kap, vagy megszűnik, akkor a referenciaszámláló csökken 1-el. -Amikor egy példányhoz tartozó referenciaszámláló eléri a 0-t, akkor automatikusan fel lehet szabadítani a hozzá tartozó memóriaterületet.
7 public int Akarmi() { TVerem v = new TVerem(); // v használata... } public int Akarmi() { TVerem v = new TVerem(); // v használata... } Példány memória allokálása Referenciaszámláló = 1 A lokális ‘v’ változó megszűnik létezni Referenciaszámláló = 0 Példány törlése a memóriából automatikusan! A példány törlése során 1: meg kell hívni a destruktorát, hogy ‘éretsítődjön’ a példány, hogy törlése fog kerülni, amit még akar az ‘utolsó szó jogán’ azt tegye meg 2: a lefoglalt memória felszabadítása
8 Az ilyen referenciaszámlálás-elvű programozási nyelveken a destruktort már nem explicit módon hívja meg a programozó, hanem a futtató rendszer hívja meg automatikusan (implicit). A futtató rendszer viszont nem fog neki paramétert átadni (nem is tudna honnan), ezért itt már nincs értelme a destruktort paraméteresen megírni (nem is szabad).
9 A referenciaszámlálás nem okoz komolyabb mérvű lassulást a program futása során! Ugyanakkor a programozók azonnal megszerették, mert megszűnt a memóriaszivárgás, egyszerűsödött a program írása, egy hibalehetőség megszűnt (egy gonddal kevesebb). Ugyanakkor jegyezzük meg, hogy a referenciaszámlálás bonyolultabb esetben sajnos egyszerűen semmit sem ér !
10 Tegyük fel, hogy egy A példány hivatkozik egy B példányra, és a B is hivatkozik az A példányra ( kölcsönös hivatkozás ). Ilyenek pl. a ciklikusan láncolt lista elemei! ”A” példány ”B” példány változó = ”A” példány; Referenciaszámláló = 2 Referenciaszámláló = 1
11 Ha a változó megszűnik, vagy már nem erre a példányra hivatkozik, akkor az ”A” példány a programból már elérhetetlenné vált. Ugyanakkor a referenciaszámlálója még mindig 1! ”A” példány ”B” példány változó = null; Referenciaszámláló = 1 Ekkor az ”A” és a ”B” példány is beragadt a memóriában! Pedig ezt akartuk kikerülni…
12 (#3) Egy komolyabb programban a példányok egymásra is hivatkoznak, és ez egy komoly hivatkozási hálót ( gráfot ) hoz létre. Egy egyszerű referencia számlálás kevés a felesleges példányok felfedezésére és kiszűrésére. a program ‘élő’ változói hivatkoznak a példányokra a program ‘élő’ változói hivatkoznak a példányokra
13 A megoldás, hogy a gráfot be kell járni (gráfbejáró algoritmussal, pl szélességi vagy mélységi bejárás) a program változóiból kiindulva. Amely példányhoz nem lehet eljutni az élek ( hivatkozások ) mentén, azok a példányok feleslegesek, és ki lehet őket törölni. Ezt a megvalósítást már nem referencia-számlálásnak nevezzük, hanem ‘szemétgyüjtésnek’. Szemét = garbage Gyűjtő = collector
Obj1 NextObjPtr Amikor új példányt hozunk létre, akkor egy halom (heap) területen lefoglalunk egy egybefüggő szakaszt. Ezen területen az első szabad helyet egy változó jelöli, őt kell növelni a lefoglalandó terület méretével:
Gyökerek Aztán jön a GC, elemzi a hivatkozási gráfot, és kiszúrja a felesleges példányokat. A gyökerek a program aktuálisan élő változói: NextObjPtr Obj4 Obj3 Obj2 Obj1 1. Gyökerek 2. Elérhetőségi gráf 3. Takarítás 4. Tömörítés 5. Mutatók frissítése
16 Ha a futtató rendszer GC-t használ, akkor a destruktort hívása automatikusan történik meg, amikor eljut odáig a GC. Ez általában nem azonnal történik meg, mint ahogy a példány ‘szemétté’ válik, időre van szükség. Ezért a destruktort hívásának időpontja nemdeterminisztikus. Az sem determinisztikus, hogy az ”A” vagy a ”B” példányra hívódik-e meg először a destruktor, és szabadítódik fel a memória. ”A” példány ”B” példány
17 C#-ban a destruktort a programunkkal párhuzamosan, egy külön szálon fut. Másodpercenként több száz millió példányt képes felfedezni, és felszabadítani ha kell. Valamelyest lassítja a program futását, de ez elhanyagolható azon előny mellett, hogy garantáltan nincs memóriaszivárgás a programban, és a programozó nem véthet ilyen jellegű hibát. Pl: egy GC-vel ellátott nyelven a láncolt lista minden elemének törlése: A többit a GC végzi FejElem = null;
18 Nagyon ritkán írunk destruktort, mert: - a pluszban lefoglalt memória is példány, és a GC majd azt is fel fogja szabadítani - a hálózati kapcsolat létrehozásához is példányosítani kell egy ‘hálózati kapcsolat’ osztályt, és ezen példányt a GC meg fogja találni, és meghívja rá az ő destruktorát, és akkor a hálózati kapcsolat automatikusan lezárja magát stb… Ezért csak nagyon speciális esetben kell nekünk magunknak explicit módon felszabadítani erőforrást.
19 Valójában a C#-ban nincs is destruktor, csak destruktornak kinéző metódus. A destruktor szót egyéb OOP nyelvekből vette át a C#. A destruktort valójában a Finalize() metódus override-ja. class TSajat { ~TSajat() {... } } class TSajat { ~TSajat() {... } } class TSajat { protected override Finalize() {... } class TSajat { protected override Finalize() {... }
20 A C# fordítóprogramja a destruktorunkat automatikusan Finalize()-nek olvassa. De nem engedi meg, hogy közvetlenül a Finalize-t definiáljuk felül. Ha az ős osztálynak is van destruktora, akkor az le fog futni, mielőtt a mi destruktorunk elindulna hasonlóan a konstruktorok hívási láncához.