4. diel - Referenčnej a hodnotové dátové typy
V predchádzajúcom cvičení, Riešené úlohy k 3. lekcii OOP v C # .NET, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.
V minulej lekcii, Riešené úlohy k 3. lekcii OOP v C # .NET , 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, ktoré sa v niektorých ohľadoch správajú
inak, než typy hodnotové (napr. int
). 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ť.
Zopakujme si pre istotu ešte raz, čo sú to hodnotovej typy. Všeobecne sú
to jednoduché štruktúry, napr. Jedno číslo, jeden znak.
Väčšinou sa chce, aby sme s nimi pracovali čo
najrýchlejšie, v programe sa ich vyskytuje veľmi
veľa a zaberajú málo miesta. V anglickej
literatúre sú často opisované slovami light-weight. Majú pevnú
veľkosť. Príkladom sú napr. int
, float
,
double
, char
, bool
a ďalšie.
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. Táto malá a rýchla pamäť je využívaná na ukladanie lokálnych premenných hodnotového typu (až na výnimky pri opakovaniach, ktorými sa nebudeme zaoberať). Premennou si v nej môžeme predstaviť asi takto:
Na obrázku je znázornená pamäť, ktorú môže naše aplikácie
využívať. V aplikácii sme si vytvorili premennú a
typu
int
. Jej hodnota je 56
a uložila sa nám priamo do
zásobníka. Kód by mohol vyzerať takto:
int a = 56;
Môžeme to chápať tak, že premenná a
má pridelenú časť
pamäte v zásobníku (veľkosti dátového typu int
, teda 32
bitov), v ktorej je uložená hodnota 56
.
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 { public int vek; public string jmeno; public Uzivatel(string jmeno, int vek) { this.jmeno = jmeno; this.vek = vek; } public override string ToString() { return jmeno; } }
Trieda má 2 jednoduché verejné atribúty, 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:
int a = 56; Uzivatel u = new Uzivatel("Jan Novák", 28);
Premenná u
je teraz referenčného typu. Pozrime sa na novú
situáciu v pamäti:
Vidíme, že objekt (premenná referenčného dátového typu) sa už neukladá do zásobníka, ale do pamäte zvanej halda. Je to z toho dôvodu, že objekt je spravidla zložitejšie ako hodnotový dátový typ (väčšinou obsahuje hneď niekoľko ďalších atribútov) a tiež zaberá viac miesta 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á.
Premenné referenčného typu 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.
Napr. v C ++ je veľký rozdiel medzi pojmom ukazovateľ a referencie. C # žiadne ukazovatele našťastie nemá a používa termín referencie, tie sa paradoxne princípom podobajú skôr ukazovateľom v C ++. Pojmy ukazovateľ a referencie tu spomenuté teda znamenajú referenciu v zmysle C # a nemajú s C ++ nič spoločné.
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 malý hodnotový typ s referenciou na objekt namiesto toho, aby sme všeobecne pamäťovo náročný objekt kopírovali. Toto si vzápätí ukážeme.
- Pomocou referencií môžeme jednoducho vytvárať štruktúry s dynamickou
veľkosťou, napr. Štruktúry podobné poli, do ktorých môžeme za behu
vkladať nové prvky. Tie sú na seba navzájom odkazované referenciami, ako
reťaz objektov.
Založme si 2 premenné typu
int
a 2 premenné typuUzivatel
:
int a = 56; int b = 28; 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 a
premennú b
. Rovnako tak priradíme aj premennú v
do premennej u
. Hodnotový typ sa v zásobníku len skopíruje, pri objekte sa skopíruje iba
referencie (čo je vlastne tiež hodnotový typ), ale objekt máme stále len
jeden. V kóde vykonáme teda toto:
int a = 56; int b = 28; Uzivatel u = new Uzivatel("Jan Novák", 28); Uzivatel v = new Uzivatel("Josef Nový", 32); a = b; 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 všetky štyri
premenné vypísať pred a po zmene. Pretože budeme výpis volať viackrát,
napíšem ho trochu úspornejšie. Mohli by sme dať výpis do metódy, ale
ešte nevieme, ako deklarovať metódy priamo v Program.cs
a
spravidla sa to ani veľmi nerobí, pre vážnejšie prácu by sme si mali
urobiť triedu. Upravme teda kód na nasledujúce:
{CSHARP_CONSOLE} // založenie premenných int a = 56; int b = 28; Uzivatel u = new Uzivatel("Jan Novák", 28); Uzivatel v = new Uzivatel("Josef Nový", 32); Console.WriteLine("a: {0}\nb: {1}\nu: {2}\nv: {3}\n", a, b, u, v); // priraďovanie a = b; u = v; Console.WriteLine("a: {0}\nb: {1}\nu: {2}\nv: {3}\n", a, b, u, v); Console.ReadKey(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Uzivatel { public int vek; public string jmeno; public Uzivatel(string jmeno, int vek) { this.jmeno = jmeno; this.vek = vek; } public override string ToString() { return jmeno; } } {/CSHARP_OOP}
Na výstupe programu zatiaľ rozdiel medzi hodnotovým a referenčným typom nespoznáme:
Konzolová aplikácia
a: 56
b: 28
u: Jan Novák
v: Josef Nový
a: 28
b: 28
u: Josef Nový
v: Josef Nový
Avšak vieme, že kým v a
a b
sú naozaj 2 rôzne
čísla s rovnakou hodnotou, v u
a v
je ten istý
objekt. 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:
{CSHARP_CONSOLE} // založenie premenných int a = 56; int b = 28; Uzivatel u = new Uzivatel("Jan Novák", 28); Uzivatel v = new Uzivatel("Josef Nový", 32); Console.WriteLine("a: {0}\nb: {1}\nu: {2}\nv: {3}\n", a, b, u, v); // priraďovanie a = b; u = v; Console.WriteLine("a: {0}\nb: {1}\nu: {2}\nv: {3}\n", a, b, u, v); // zmena v.jmeno = "John Doe"; Console.WriteLine("u: {0}\nv: {1}\n", u, v); Console.ReadKey(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Uzivatel { public int vek; public string jmeno; public Uzivatel(string jmeno, int vek) { this.jmeno = jmeno; this.vek = vek; } public override string ToString() { return jmeno; } } {/CSHARP_OOP}
Zmenili sme objekt v premennej v
a znovu vypíšeme
u
a v
:
Konzolová aplikácia
a: 56
b: 28
u: Jan Novák
v: Josef Nový
a: 28
b: 28
u: Josef Nový
v: Josef Nový
u: John Doe
v: John Doe
Spolu so zmenou v
sa zmení aj u
, pretože
premenné ukazujú na ten istý objekt. Ak sa pýtate, ako vytvoriť naozajstnú
kópiu objektu, tak najjednoduchšie je objekt znova vytvoriť pomocou
konstruktoru a dať do neho rovnaké dáta. Ďalej môžeme použiť klonovanie,
ale o tom zas až niekedy inokedy. 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ť. Doteraz sme to tak vlastne robili a nemali sme s tým problém, pekne sme do zdrojového kódu napísali potrebné premenné. Čoskoro sa ale budeme stretávať s aplikáciami (a už sme sa vlastne i stretli), kedy nebudeme pred spustením presne vedieť, koľko pamäte budeme potrebovať. Spomeňte si na program, ktorý vytvoril priemer zadané hodnoty v poli. Na počet hodnôt sme sa užívateľa opýtali až za behu programu. CLR teda musel za behu programu poľa v pamäti založiť. 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 alebo začať preskúmavať pamäť počítača, ktorá je v binárce. 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 nekontrolovanému správanie našej aplikácie a môže to dopadnúť aj takto:
Môj kolega raz povedal: "Ľudský mozog sa nedokáže starať ani o správu pamäte vlastné, nieto aby riešil memory management programu." Mal samozrejme pravdu, 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 C # a Java . 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 hodia C #, kvôli možnosti používať .NET a hlavne automatické správe pamäte. Používať .NET bolo umožnené aj v C ++, hovoríme o tzv. Managed C ++, kde výsledná aplikácia používala garbage collector. Projekt sa však neuchytil, pretože C ++ tak už nemalo žiadne výhody oproti C #, ktorý je modernejší.
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 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
. Referenčné typy môžu, na rozdiel od
hodnotových, nadobúdať špeciálne hodnoty a to null
.
Kľúčové slovo null
označuje, že referencie neukazuje na
žiadne dáta. Keď nastavíme premennú v
na null
,
zrušíme iba tú jednu 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:
{CSHARP_CONSOLE} // založenie premenných int a = 56; int b = 28; Uzivatel u = new Uzivatel("Jan Novák", 28); Uzivatel v = new Uzivatel("Josef Nový", 32); Console.WriteLine("a: {0}\nb: {1}\nu: {2}\nv: {3}\n", a, b, u, v); // priraďovanie a = b; u = v; Console.WriteLine("a: {0}\nb: {1}\nu: {2}\nv: {3}\n", a, b, u, v); // zmena v.jmeno = "John Doe"; v = null; Console.WriteLine("u: {0}\nv: {1}\n", u, v); Console.ReadKey(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Uzivatel { public int vek; public string jmeno; public Uzivatel(string jmeno, int vek) { this.jmeno = jmeno; this.vek = vek; } public override string ToString() { return jmeno; } } {/CSHARP_OOP}
výstup:
Konzolová aplikácia
a: 56
b: 28
u: Jan Novák
v: Josef Nový
a: 28
b: 28
u: Josef Nový
v: Josef Nový
u: John Doe
v:
Vidíme, že objekt stále existuje a ukazuje na neho premenná
u
, v premennej v
už nie je referencie. Hodnota
null
sa bohato využíva ako vo vnútri .NET, tak v databázach. K
referenčným typom sa ešte raz vrátime. V budúcej lekcii, Riešené úlohy k 4. lekcii OOP v C # .NET , 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 C # .NET, 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é 853x (25.3 kB)
Aplikácia je vrátane zdrojových kódov v jazyku C#