4. diel - Odkazy na objekty, ich kopírovanie a garbage collector
V predchádzajúcom cvičení, Riešené úlohy k 3. lekcii OOP v Dart, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.
V minulej lekcii, Riešené úlohy k 3. lekcii OOP v Dart , sme si vytvorili svoj prvý poriadny objekt, bola ním hracia kocka. Začíname pracovať s objektmi a objekty sú referenčnými dátovými typy. Je dôležité, aby sme presne vedeli, čo sa vo vnútri programu deje, inak by nás v budúcnosti mohlo všeličo prekvapiť.
Aplikácia (resp. Jej vlákno) má operačným systémom pridelenú pamäť v podobe tzv. Zásobníka (stack). Jedná sa o veľmi rýchlu pamäť s priamym prístupom, jej veľkosť aplikácie nemôže ovplyvniť, prostriedky sú prideľované operačným systémom.
Vytvorme si novú konzolovú aplikáciu a pridajme si k nej jednoduchú triedu, ktorá bude reprezentovať užívateľa nejakého systému. Pre názornosť vypustím komentáre a nebudem riešiť viditeľnosti:
class Uzivatel { int vek; String jmeno; Uzivatel(this.jmeno, this.vek); @override String toString() { return jmeno; } }
Trieda má 2 jednoduché verejné vlastnosti, konštruktor a preťažený
toString()
, aby sme používateľa mohli jednoducho vypisovať. Do
nášho pôvodného programu pridajme vytvorení inštancie tejto triedy:
Uzivatel u = new Uzivatel('Jan Novák', 28);
Premenná u
obsahuje odkaz na objekt. Pozrime sa na situáciu v
pamäti:
Zásobník aj halda sa nachádzajú v pamäti RAM. Rozdiel je v prístupe a veľkosti. Halda je prakticky neobmedzená pamäť, ku ktorej je však prístup zložitejšia a tým pádom pomalší. Naopak zásobník je pamäť rýchla, ale veľkostne obmedzená.
Objekty sú v pamäti uložené vlastne na dvakrát, raz v zásobníku a raz v halde. V zásobníku je uložená iba tzv. Referencie, teda odkaz do haldy, kde sa potom nachádza naozajstný objekt.
Môžete sa pýtať, prečo je to takto urobené. Dôvodov je hneď niekoľko, poďme si niektoré vymenovať:
- Miesto vo stacku je obmedzené.
- Keď budeme chcieť použiť objekt viackrát (napr. Ho odovzdať ako parameter do niekoľkých metód), nemusíme ho v programe odovzdávať ako kópiu. Odovzdáme iba referenciu na objekt namiesto toho, aby sme všeobecne pamäťovo náročný objekt kopírovali. Toto si vzápätí ukážeme.
Založme si 2 premenné typu Uzivatel
:
Uzivatel u = new Uzivatel('Jan Novák', 28); Uzivatel v = new Uzivatel('Josef Nový', 32);
Situácia v pamäti bude nasledovné:
Teraz skúsme priradiť do premennej u
premennú v
.
Pri objekte sa skopíruje iba odkaz na objekt, ale objekt máme stále len
jeden. V kóde vykonáme teda toto:
Uzivatel u = new Uzivatel('Jan Novák', 28); Uzivatel v = new Uzivatel('Josef Nový', 32); u = v;
V pamäti bude celá situácia vyzerať nasledovne:
Presvedčte sa o tom, aby ste videli, že to naozaj tak je:) Najprv si
necháme obe dve premenné vypísať pred a po zmene. Navyše si vypíšeme aj
tzv. Hash kód cez vlastnosť hashCode
, aby sme videli, že sa
jedná o rôzne objekty. Pretože budeme výpis volať viackrát, napíšem ho
trochu úspornejšie. Upravme teda kód na nasledujúce:
// založení proměnných Uzivatel u = new Uzivatel('Jan Novák', 28); Uzivatel v = new Uzivatel('Josef Nový', 32); print('u: $u ${u.hashCode}'); print('v: $v ${v.hashCode}\n'); // přiřazení u = v; print('u: $u ${u.hashCode}'); print('v: $v ${v.hashCode}\n');
Výstup programu:
Konzolová aplikácia
Jan Novák: 41841878
Josef Nový: 135857904
Josef Nový: 135857904
Josef Nový: 135857904
Poďme zmeniť meno používateľa v
a podľa našich
predpokladov by sa mala zmena prejaviť aj v premennej u
. K
programu pripíšeme:
v.jmeno = 'John Doe'; print('u: $u ${u.hashCode}'); print('v: $v ${v.hashCode}\n');
Zmenili sme objekt v premennej v
a znovu vypíšeme
u
a v
:
Konzolová aplikácia
u: Jan Novák 1051461893
v: Josef Nový 323740234
u: Josef Nový 323740234
v: Josef Nový 323740234
u: John Doe 323740234
v: John Doe 323740234
Spolu so zmenou v
sa zmení aj u
, pretože
premenné ukazujú na ten istý objekt. Pripomeňme si situáciu v pamäti ešte
raz a zamerajme sa na Jána Nováka.
Čo sa sním stane? "Zožerie" ho tzv. Garbage collector.
Garbage collector a dynamická správa pamäte
Pamäť môžeme v programoch alokovať staticky, to znamená, že v zdrojovom kóde vopred určíme, koľko jej budeme používať. Ale tiež nemusíme určiť koľko pamäte budeme potrebovať. V tomto prípade hovoríme o dynamickej správe pamäte.
V minulosti, hlavne v časoch jazykov C, Pascal a C ++, sa na tento účel používali tzv. Pointer, čiže priame ukazovatele do pamäte. Napospol to fungovalo tak, že sme si povedali operačnému systému o kus pamäti o určitej veľkosti. On ju pre nás vyhradil a dal nám jej adresu. Na toto miesto v pamäti sme mali pointer, cez ktorý sme s pamäťou pracovali. Problém bol, že nikto nestrážil, čo do pamäti dávame (ukazovateľ smeroval na začiatok vyhradeného priestoru). Keď sme tam dali niečo väčšieho, skrátka sa to rovnako uložilo a prepísala sa dáta za naším priestorom, ktorá patrila napríklad inému programu alebo operačnému systému (v tom prípade by našu aplikáciu OS asi zabil - zastavil). Často sme si však my v pamäti prepísali nejaká ďalšie dáta nášho programu a program sa začal správať chaoticky. Predstavte si, že si uložíte používateľa do poľa a v tej chvíli sa vám zrazu zmení farba užívateľského prostredia, teda niečo, čo s tým vôbec nesúvisí. Hodiny strávite tým, že kontrolujete kód pre zmenu farby, potom zistíte, že je chyba v založenia používateľa, kedy dôjde k pretečeniu pamäte a prepísanie hodnôt farby. Keď naopak nejaký objekt prestaneme používať, musíme po ňom miesto sami uvoľniť, ak to neurobíme, pamäť zostane blokovaná. Pokiaľ toto robíme napr. V nejakej metóde a zabudneme pamäť uvoľňovať, naše aplikácie začne padať, prípadne zasekne celý operačný systém. Takáto chyba sa opäť zle hľadá, prečo program prestane po niekoľkých hodinách fungovať? Kde tú chybu v niekoľkých tisícoch riadkov kódu vôbec hľadať? Nemáme jedinú stopu, nemôžeme sa ničoho chytiť, musíme prejsť celý program riadok po riadku. Brrr. Podobný problém nastane, keď si niekde pamäť uvoľníme a následne pointer opäť použijeme (zabudneme, že je uvoľnený, to sa môže ľahko stať), povedie niekam, kde je už uloženého niečo iné a tieto dáta budú opäť prepísané. Povedie to k nekontrolovateľnému správanie našej aplikácie a môže to dopadnúť aj takto:
Až na malú skupinu géniov ľudí prestalo baviť riešiť neustále a nezmyselné chyby. Za cenu mierneho zníženia výkonu vznikli riadené jazyky (managed) s tzv. Garbage collector, jedným z nich je aj Dart. C ++ sa samozrejme naďalej používa, ale len na špecifické programy, napr. Časti operačného systému alebo 3D enginy komerčných hier, kde je potreba z počítača dostať maximálny výkon. Na 99% všetkých ostatných aplikácií sa viac hodí Dart alebo iný riadený jazyk, kvôli automatické správe pamäte.
Garbage collector je vlastne program, ktorý beží paralelne s našou aplikáciou, v samostatnom vlákne. Občas sa spustí a pozrie sa, na ktoré objekty už v pamäti nevedú žiadne referencie. Tie potom odstráni. Strata výkonu je minimálna a značne to zníži percento samovrážd programátorov, ladiacich po večeroch rozbité pointera. Zapnutie a vypnutie GC môžeme dokonca z kódu ovplyvniť, aj keď to nie je v 99% prípadov vôbec potrebné. Pretože je jazyk riadený a nepracujeme s priamymi Pointer, nie je vôbec možné pamäť nejako narušiť, nechať ju pretiecť a podobne, interpret sa o pamäť automaticky stará.Hodnota null
Posledná vec, o ktorej sa zmienime, je tzv. Hodnota null
, o
ktorej sme sa už raz zmieňovali. Referenčná typy (tj. Všetko v Dart) môžu
nadobúdať špeciálne hodnoty null
. null
je
kľúčové slovo a označuje, že referencie neukazuje na žiadne dáta. Keď
nastavíme premennú v
na null
, zrušíme iba
referenciu. Pokiaľ na náš objekt existuje ešte nejaká referencie, bude aj
naďalej existovať. Ak nie, bude uvoľnený GC. Zmeňme ešte posledná riadky
nášho programu na:
v = null; print('u: $u ${u.hashCode}'); print('v: $v ${v.hashCode}\n');
Výstup programu:
Konzolová aplikácia
u: Jan Novák 478181311
v: Josef Nový 1887001
u: Josef Nový 1887001
v: Josef Nový 1887001
u: John Doe 1887001
v: John Doe 1887001
u: John Doe 1887001
v: null 2011
Vidíme, že objekt stále existuje a ukazuje na neho premenná
u
, v premennej v
už nie je referencie.
Kopírovanie objektov
Ako ste si iste všimli, uložiť čokoľvek len tak do premennej nám
nevytvorí kópiu v pravom slova zmysle, len sa nám zmení referencie. Ak by
sme chceli "pravú" kópiu (tzv. Hlbokú kópiu), musíme všetky dáta ručne
prekopírovať. Pre základné typy ako čísla, reťazce atp. to nebude
problém - tieto objekty sú tzv. immutable, tj. nemenné a pri každej
zmene sa vytvorí nový objekt. Avšak pre komplikovanejšie dátové typy, ako
je napríklad zoznam, musíme ručne nakopírovať všetky prvky zoznamu, inak
by sa nám skopírovala len referencie. Dart je však na toto tiež pripravený
a tak stačí využiť pomenovaný konštruktor List.from()
.
Do našej triedy Uzivatel
si teda tiež pridáme pomenovaný
konštruktor, ktorý bude mať za úlohu vytvoriť pravú kópiu objektu. Budeme
sa držať rovnakého pomenovania ako má zoznam, a teda vytvoríme konštruktor
Uzivatel.from()
:
Uzivatel.from(Uzivatel u) { vek = u.vek; jmeno = u.jmeno; }
Všetko si vyskúšame, či nám kópie funguje naozaj správne:
u = new Uzivatel('Toník Bystrý', 14); v = new Uzivatel.from(u); // vyzkoušejte zaměnit za v = u; u.jmeno = 'Honza Nářez'; print('u: $u ${u.hashCode}'); print('v: $v ${v.hashCode}\n');
Výstup programu:
Konzolová aplikácia
u: Honza Nářez 897047735
v: Toník Bystrý 356399476
V budúcej lekcii, Riešené úlohy k 4. lekcii OOP v Dart , si zas niečo praktické naprogramujeme, nech si vedomosti zažijeme. Prezradím, že pôjde o objekt bojovníka do našej arény. To je zatiaľ všetko.:)
V nasledujúcom cvičení, Riešené úlohy k 4. lekcii OOP v Dart, si precvičíme nadobudnuté skúsenosti z predchádzajúcich lekcií.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 5x (2.23 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Dart