Lekce 4 - Odkazy na objekty, jejich kopírování a garbage collector
V předešlém cvičení, Řešené úlohy k 3. lekci OOP v Dartu, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
V minulé lekci, Řešené úlohy k 3. lekci OOP v Dartu, jsme si vytvořili svůj první pořádný objekt, byla jím hrací kostka. Začínáme pracovat s objekty a objekty jsou referenčními datovými typy. Je důležité, abychom přesně věděli, co se uvnitř programu děje, jinak by nás v budoucnu mohlo leccos překvapit.
Aplikace (resp. její vlákno) má operačním systémem přidělenou paměť v podobě tzv. zásobníku (stack). Jedná se o velmi rychlou paměť s přímým přístupem, její velikost aplikace nemůže ovlivnit, prostředky jsou přidělovány operačním systémem.
Vytvořme si novou konzolovou aplikaci a přidejme si k ní jednoduchou třídu, která bude reprezentovat uživatele nějakého systému. Pro názornost vypustím komentáře a nebudu řešit viditelnosti:
class Uzivatel { int vek; String jmeno; Uzivatel(this.jmeno, this.vek); @override String toString() { return jmeno; } }
Třída má 2 jednoduché veřejné vlastnosti, konstruktor a přetížený
toString()
, abychom uživatele mohli jednoduše vypisovat. Do
našeho původního programu přidejme vytvoření instance této třídy:
Uzivatel u = new Uzivatel('Jan Novák', 28);
Proměnná u
obsahuje odkaz na objekt. Podívejme se na situaci
v paměti:

Zásobník i halda se nacházejí v paměti RAM. Rozdíl je v přístupu a velikosti. Halda je prakticky neomezená paměť, ke které je však přístup složitější a tím pádem pomalejší. Naopak zásobník je paměť rychlá, ale velikostně omezená.
Objekty jsou v paměti uloženy vlastně nadvakrát, jednou v zásobníku a jednou v haldě. V zásobníku je uložena pouze tzv. reference, tedy odkaz do haldy, kde se poté nalézá opravdový objekt.
Můžete se ptát, proč je to takto udělané. Důvodů je hned několik, pojďme si některé vyjmenovat:
- Místo ve stacku je omezené.
- Když budeme chtít použít objekt vícekrát (např. ho předat jako parametr do několika metod), nemusíme ho v programu předávat jako kopii. Předáme pouze referenci na objekt místo toho, abychom obecně paměťově náročný objekt kopírovali. Toto si vzápětí ukážeme.
Založme si 2 proměnné typu Uzivatel
:
Uzivatel u = new Uzivatel('Jan Novák', 28); Uzivatel v = new Uzivatel('Josef Nový', 32);
Situace v paměti bude následující:

Nyní zkusme přiřadit do proměnné u
proměnnou
v
. U objektu se zkopíruje pouze odkaz na objekt, ale objekt máme
stále jen jeden. V kódu vykonáme tedy toto:
Uzivatel u = new Uzivatel('Jan Novák', 28); Uzivatel v = new Uzivatel('Josef Nový', 32); u = v;
V paměti bude celá situace vypadat následovně:

Přesvědčme se o tom, abyste viděli, že to opravdu tak je Nejprve si necháme obě dvě
proměnné vypsat před a po změně. Navíc si vypíšeme i tzv. hash kód
přes vlastnost
hashCode
, abychom viděli, že se jedná o různé
objekty. Protože budeme výpis volat vícekrát, napíši ho poněkud
úsporněji. Upravme tedy kód na následující:
// 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á aplikace
Jan Novák: 41841878
Josef Nový: 135857904
Josef Nový: 135857904
Josef Nový: 135857904
Pojďme změnit jméno uživatele v
a dle našich předpokladů
by se měla změna projevit i v proměnné u
. K programu
připíšeme:
v.jmeno = 'John Doe'; print('u: $u ${u.hashCode}'); print('v: $v ${v.hashCode}\n');
Změnili jsme objekt v proměnné v
a znovu vypíšeme
u
a v
:
Konzolová aplikace
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 se změnou v
se změní i u
, protože
proměnné ukazují na ten samý objekt. Připomeňme si situaci v paměti
ještě jednou a zaměřme se na Jana Nováka.

Co se sním stane? "Sežere" ho tzv. Garbage collector.

Garbage collector a dynamická správa paměti
Paměť můžeme v programech alokovat staticky, to znamená, že ve zdrojovém kódu předem určíme, kolik jí budeme používat. Ale také nemusíme určit kolik paměti budeme potřebovat. V tomto případě hovoříme o dynamické správě paměti.
V minulosti, hlavně v dobách jazyků C, Pascal a C++, se k tomuto účelu používaly tzv. pointery, neboli přímé ukazatele do paměti. Vesměs to fungovalo tak, že jsme si řekli operačnímu systému o kus paměti o určité velikosti. On ji pro nás vyhradil a dal nám její adresu. Na toto místo v paměti jsme měli pointer, přes který jsme s pamětí pracovali. Problém byl, že nikdo nehlídal, co do paměti dáváme (ukazatel směřoval na začátek vyhrazeného prostoru). Když jsme tam dali něco většího, zkrátka se to stejně uložilo a přepsala se data za naším prostorem, která patřila třeba jinému programu nebo operačnímu systému (v tom případě by naši aplikaci OS asi zabil - zastavil). Často jsme si však my v paměti přepsali nějaká další data našeho programu a program se začal chovat chaoticky. Představte si, že si uložíte uživatele do pole a v tu chvíli se vám najednou změní barva uživatelského prostředí, tedy něco, co s tím vůbec nesouvisí. Hodiny strávíte tím, že kontrolujete kód pro změnu barvy, poté zjistíte, že je chyba v založení uživatele, kdy dojde k přetečení paměti a přepsání hodnot barvy. Když naopak nějaký objekt přestaneme používat, musíme po něm místo sami uvolnit, pokud to neuděláme, paměť zůstane blokovaná. Pokud toto děláme např. v nějaké metodě a zapomeneme paměť uvolňovat, naše aplikace začne padat, případně zasekne celý operační systém. Taková chyba se opět špatně hledá, proč program přestane po několika hodinách fungovat? Kde tu chybu v několika tisících řádků kódu vůbec hledat? Nemáme jedinou stopu, nemůžeme se ničeho chytit, musíme projet celý program řádek po řádku. Brrr. Podobný problém nastane, když si někde paměť uvolníme a následně pointer opět použijeme (zapomeneme, že je uvolněný, to se může lehce stát), povede někam, kde je již uloženého něco jiného a tato data budou opět přepsána. Povede to k nekontrolovatelnému chování naší aplikace a může to dopadnout i takto:

Až na malou skupinu géniů lidi přestalo bavit řešit neustálé a nesmyslné chyby. Za cenu mírného snížení výkonu vznikly řízené jazyky (managed) s tzv. garbage collectorem, jedním z nich je i Dart. C++ se samozřejmě nadále používá, ale pouze na specifické programy, např. části operačního systému nebo 3D enginy komerčních her, kde je potřeba z počítače dostat maximální výkon. Na 99 % všech ostatních aplikací se více hodí Dart nebo jiný řízený jazyk, kvůli automatické správě paměti.

Garbage collector je vlastně program, který běží paralelně s naší aplikací, v samostatném vlákně. Občas se spustí a podívá se, na které objekty již v paměti nevedou žádné reference. Ty potom odstraní. Ztráta výkonu je minimální a značně to sníží procento sebevražd programátorů, ladících po večerech rozbité pointery. Zapnutí a vypnutí GC můžeme dokonce z kódu ovlivnit, i když to není v 99% případů vůbec potřeba. Protože je jazyk řízený a nepracujeme s přímými pointery, není vůbec možné paměť nějak narušit, nechat ji přetéct a podobně, interpret se o paměť automaticky stará.
Hodnota null
Poslední věc, o které se zmíníme, je tzv. hodnota null
, o
které jsme se již jednou zmiňovali. Referenční typy (tj. všechno v Dartu)
mohou nabývat speciální hodnoty null
. null
je
klíčové slovo a označuje, že reference neukazuje na žádná data. Když
nastavíme proměnnou v
na null
, zrušíme pouze
referenci. Pokud na náš objekt existuje ještě nějaká reference, bude i
nadále existovat. Pokud ne, bude uvolněn GC. Změňme ještě poslední
řádky našeho programu na:
v = null; print('u: $u ${u.hashCode}'); print('v: $v ${v.hashCode}\n');
Výstup programu:
Konzolová aplikace
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 něj proměnná
u
, v proměnné v
již není reference.
Kopírování objektů
Jak jste si jistě všimli, uložit cokoli jen tak do proměnné nám
nevytvoří kopii v pravém slova smyslu, jen se nám změní reference. Pokud
bychom chtěli "pravou" kopii (tzv. hlubokou kopii), musíme všechna data
ručně překopírovat. Pro základní typy jako čísla, řetězce atp. to
nebude problém - tyto objekty jsou tzv. immutable, tj. neměnné a
při každé změně se vytvoří nový objekt. Avšak pro komplikovanější
datové typy, jako je například seznam, musíme ručně nakopírovat všechny
prvky seznamu, jinak by se nám zkopírovala pouze reference. Dart je však na
toto také připraven a tak stačí využít pojmenovaný konstruktor
List.from()
.
Do naší třídy Uzivatel
si tedy také přidáme pojmenovaný
konstruktor, který bude mít za úkol vytvořit pravou kopii objektu. Budeme se
držet stejného pojmenování jako má seznam, a tedy vytvoříme konstruktor
Uzivatel.from()
:
Uzivatel.from(Uzivatel u) { vek = u.vek; jmeno = u.jmeno; }
Vše si vyzkoušíme, jestli nám kopie funguje opravdu správně:
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á aplikace
u: Honza Nářez 897047735
v: Toník Bystrý 356399476
V příští lekci, Řešené úlohy k 4. lekci OOP v Dartu, si zas něco praktického naprogramujeme, ať si
znalosti zažijeme. Prozradím, že půjde o objekt bojovníka do naší arény.
To je zatím vše.
V následujícím cvičení, Řešené úlohy k 4. lekci OOP v Dartu, si procvičíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 5x (2.23 kB)
Aplikace je včetně zdrojových kódů v jazyce Dart